diff --git a/docs/deployment.md b/docs/deployment.md
index 55f041b93592ffc642c691fca044ef82b012b0f4..8d7baf049a974cfbff0a4be73bbd22173d07776a 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -40,6 +40,7 @@ You now need to set the token.
 ```
 $ heroku config:set GITHUB_TOKEN=[YourGitHubToken]
 ```
+(or use `GITLAB_TOKEN` if appropriate)
 
 You should also set any other [Configuration Options](configuration.md) you need.
 
diff --git a/docs/design-decisions.md b/docs/design-decisions.md
index 39cd14dc9b3976d7c5856267e43107414b354584..2cdb3c1394ea357eb1cdf9afc16888eb79b847bc 100644
--- a/docs/design-decisions.md
+++ b/docs/design-decisions.md
@@ -4,17 +4,18 @@ This file documents the design choices as well as configuration options.
 
 #### Stateless
 
-No state is needed on `renovate` or GitHub side apart from what you see publicly in GitHub (branches, Pull Requests). It therefore doesn't matter if you stop/restart the script and would even still work if you had it running from two different locations, as long as their configuration was the same.
+No state storage is needed on `renovate` or GitHub/GitLab apart from what you see publicly in GitHub (branches, Pull Requests). It therefore doesn't matter if you stop/restart the script and would even still work if you had it running from two different locations, as long as their configuration was the same.
 
 #### API only
 
-So far, nothing we need to do requires git itself. e.g. we do not need to perform a git clone of the entire repository. Therefore, all operations are performed via the API.
+So far, nothing we need to do requires git directly. e.g. we do not need to perform a git clone of the entire repository. Therefore, all operations are performed via the API.
 
 ## Synchronous Operation
 
 The script current processes repositories, package files, and dependencies within them all synchronously.
-- Greatly reduces chance of hitting GitHub API limits
-- Implicitly enables any feature that results in multiple commits in the same branch
+
+- Greatly reduces chance of hitting simultaneous API rate limits
+- Implicitly enables any configuration that results in multiple commits in the same branch
 - Simplifies logging
 
 Note: Initial queries to NPM are done in parallel.
@@ -43,6 +44,8 @@ The following options apply per-package file:
 The following options apply per-repository:
 
 - Token
+- Platform
+- Endpoint
 
 The following options apply globally:
 
@@ -50,6 +53,8 @@ The following options apply globally:
 
 ## Automatic discovery of package.json locations
 
+Note: GitHub only.
+
 Default behaviour is to auto-discover all `package.json` locations in a repository and process them all.
 Doing so means that "monorepos" are supported by default.
 This can be overridden by the configuration option `packageFiles`, where you list the file paths manually (e.g. limit to just `package.json` in root of repository).
@@ -100,6 +105,8 @@ Perhaps this will be made configurable in future once requirements are understoo
 
 ## Rebasing Unmergeable Pull Requests
 
+Note: GitHub only. GitLab does not expose enough low level git API to allow this.
+
 With the default behaviour of one branch per dependency, it's often that case that a PR gets merge conflicts after an adjacent dependency update is merged. Although GitHub has added a web interface for simple merge conflicts, this is still annoying to resolve manually.
 
 `renovate` will rebase any unmergeable branches and add the latest necessary commit on top of the most recent `master` commit.
diff --git a/lib/api/gitlab.js b/lib/api/gitlab.js
new file mode 100644
index 0000000000000000000000000000000000000000..e0ce0904b9597fa35df6cecbbd74d1285401bb6a
--- /dev/null
+++ b/lib/api/gitlab.js
@@ -0,0 +1,303 @@
+const logger = require('winston');
+const glGot = require('gl-got');
+
+const config = {};
+
+module.exports = {
+  initRepo,
+  // Search
+  findFilePaths,
+  // Branch
+  branchExists,
+  getBranchPr,
+  // issue
+  addAssignees,
+  addReviewers,
+  addLabels,
+  // PR
+  findPr,
+  checkForClosedPr,
+  createPr,
+  getPr,
+  updatePr,
+  // file
+  commitFilesToBranch,
+  getFile,
+  getFileContent,
+  getFileJson,
+};
+
+// Initialize GitLab by getting base branch
+async function initRepo(repoName, token, endpoint) {
+  logger.debug(`initRepo(${repoName})`);
+  if (token) {
+    process.env.GITLAB_TOKEN = token;
+  } else if (!process.env.GITLAB_TOKEN) {
+    throw new Error(`No token found for GitLab repository ${repoName}`);
+  }
+  if (token) {
+    process.env.GITLAB_TOKEN = token;
+  }
+  if (endpoint) {
+    process.env.GITLAB_ENDPOINT = endpoint;
+  }
+  config.repoName = repoName.replace('/', '%2F');
+  try {
+    const res = await glGot(`projects/${config.repoName}`);
+    config.defaultBranch = res.body.default_branch;
+    logger.debug(`${repoName} default branch = ${config.defaultBranch}`);
+  } catch (err) {
+    logger.error(`GitLab init error: ${JSON.stringify(err)}`);
+    throw err;
+  }
+}
+
+// Search
+
+// Returns an array of file paths in current repo matching the fileName
+async function findFilePaths(fileName) {
+  logger.verbose('Can\'t find multiple package.json files in GitLab');
+  return [fileName];
+}
+
+// Branch
+
+// Returns true if branch exists, otherwise false
+async function branchExists(branchName) {
+  logger.debug(`Checking if branch exists: ${branchName}`);
+  try {
+    const url = `projects/${config.repoName}/repository/branches/${branchName}`;
+    const res = await glGot(url);
+    if (res.statusCode === 200) {
+      logger.debug('Branch exists');
+      return true;
+    }
+    // This probably shouldn't happen
+    logger.debug('Branch doesn\'t exist');
+    return false;
+  } catch (error) {
+    if (error.statusCode === 404) {
+      // If file not found, then return false
+      logger.debug('Branch doesn\'t exist');
+      return false;
+    }
+    // Propagate if it's any other error
+    throw error;
+  }
+}
+
+// Returns the Pull Request for a branch. Null if not exists.
+async function getBranchPr(branchName) {
+  logger.debug(`getBranchPr(${branchName})`);
+  const urlString = `projects/${config.repoName}/merge_requests?state=opened`;
+  const res = await glGot(urlString);
+  logger.debug(`Got res with ${res.body.length} results`);
+  let pr = null;
+  res.body.forEach((result) => {
+    if (result.source_branch === branchName) {
+      pr = result;
+    }
+  });
+  if (!pr) {
+    return null;
+  }
+  return getPr(pr.id);
+}
+
+// Issue
+
+async function addAssignees(prNo, assignees) {
+  logger.debug(`Adding assignees ${assignees} to #${prNo}`);
+  if (assignees.length > 1) {
+    logger.error('Cannot assign more than one assignee to Merge Requests');
+  }
+  let url = `projects/${config.repoName}/merge_requests/${prNo}`;
+  url = `${url}?assignee_id=${assignees[0]}`;
+  await glGot.put(url);
+}
+
+async function addReviewers(prNo, reviewers) {
+  logger.debug(`addReviewers('${prNo}, '${reviewers})`);
+  logger.error('No reviewer functionality in GitLab');
+}
+
+async function addLabels(prNo, labels) {
+  logger.debug(`Adding labels ${labels} to #${prNo}`);
+  let url = `projects/${config.repoName}/merge_requests/${prNo}`;
+  url = `${url}?labels=${labels.join(',')}`;
+  await glGot.put(url);
+}
+
+async function findPr(branchName, prTitle, state = 'all') {
+  logger.debug(`findPr(${branchName}, ${prTitle}, ${state})`);
+  const urlString = `projects/${config.repoName}/merge_requests?state=${state}`;
+  const res = await glGot(urlString);
+  let pr = null;
+  res.body.forEach((result) => {
+    if ((!prTitle || result.title === prTitle) && result.source_branch === branchName) {
+      pr = result;
+      // GitHub uses number, GitLab uses iid
+      pr.number = pr.id;
+      pr.body = pr.description;
+      pr.displayNumber = `Merge Request #${pr.iid}`;
+      if (pr.state !== 'opened') {
+        pr.isClosed = true;
+      }
+    }
+  });
+  return pr;
+}
+
+// Pull Request
+async function checkForClosedPr(branchName, prTitle) {
+  const pr = await findPr(branchName, prTitle, 'closed');
+  if (pr) {
+    return true;
+  }
+  return false;
+}
+
+async function createPr(branchName, title, body) {
+  logger.debug(`Creating Merge Request: ${title}`);
+  const res = await glGot.post(`projects/${config.repoName}/merge_requests`, {
+    body: {
+      source_branch: branchName,
+      target_branch: config.defaultBranch,
+      title,
+      description: body,
+    },
+  });
+  const pr = res.body;
+  pr.number = pr.id;
+  pr.displayNumber = `Merge Request #${pr.iid}`;
+  return pr;
+}
+
+async function getPr(prNo) {
+  logger.debug(`getPr(${prNo})`);
+  const url = `projects/${config.repoName}/merge_requests/${prNo}`;
+  const pr = (await glGot(url)).body;
+  // Harmonize fields with GitHub
+  pr.number = pr.id;
+  pr.displayNumber = `Merge Request #${pr.iid}`;
+  pr.body = pr.description;
+  if (pr.state === 'closed' || pr.state === 'merged') {
+    logger.debug('pr is closed');
+    pr.isClosed = true;
+  }
+  if (pr.merge_status === 'cannot_be_merged') {
+    logger.debug('pr cannot be merged');
+    pr.isUnmergeable = true;
+  }
+  // We can't rebase through GitLab API
+  pr.canRebase = false;
+  return pr;
+}
+
+async function updatePr(prNo, title, body) {
+  await glGot.put(`projects/${config.repoName}/merge_requests/${prNo}`, {
+    body: {
+      title,
+      description: body,
+    },
+  });
+}
+
+// Generic File operations
+
+async function getFile(filePath, branchName = config.defaultBranch) {
+  const res = await glGot(`projects/${config.repoName}/repository/files?file_path=${filePath}&ref=${branchName}`);
+  return res.body.content;
+}
+
+async function getFileContent(filePath, branchName) {
+  try {
+    const file = await getFile(filePath, branchName);
+    return new Buffer(file, 'base64').toString();
+  } catch (error) {
+    if (error.statusCode === 404) {
+      // If file not found, then return null JSON
+      return null;
+    }
+    // Propagate if it's any other error
+    throw error;
+  }
+}
+
+async function getFileJson(filePath, branchName) {
+  try {
+    const fileContent = await getFileContent(filePath, branchName);
+    return JSON.parse(fileContent);
+  } catch (error) {
+    if (error.statusCode === 404) {
+      // If file not found, then return null JSON
+      return null;
+    }
+    // Propagate if it's any other error
+    throw error;
+  }
+}
+
+async function createFile(branchName, filePath, fileContents, message) {
+  await glGot.post(`projects/${config.repoName}/repository/files`, {
+    body: {
+      file_path: filePath,
+      branch_name: branchName,
+      commit_message: message,
+      encoding: 'base64',
+      content: new Buffer(fileContents).toString('base64'),
+    },
+  });
+}
+
+async function updateFile(branchName, filePath, fileContents, message) {
+  await glGot.put(`projects/${config.repoName}/repository/files`, {
+    body: {
+      file_path: filePath,
+      branch_name: branchName,
+      commit_message: message,
+      encoding: 'base64',
+      content: new Buffer(fileContents).toString('base64'),
+    },
+  });
+}
+
+// Add a new commit, create branch if not existing
+async function commitFilesToBranch(
+  branchName,
+  files,
+  message,
+  parentBranch = config.defaultBranch) {
+  logger.debug(`commitFilesToBranch('${branchName}', files, message, '${parentBranch})'`);
+  if (branchName !== parentBranch) {
+    const isBranchExisting = await branchExists(branchName);
+    if (isBranchExisting) {
+      logger.debug(`Branch ${branchName} already exists`);
+    } else {
+      logger.debug(`Creating branch ${branchName}`);
+      await createBranch(branchName);
+    }
+  }
+  for (const file of files) {
+    const existingFile = await getFileContent(file.name, branchName);
+    if (existingFile) {
+      logger.debug(`${file.name} exists - updating it`);
+      await updateFile(branchName, file.name, file.contents, message);
+    } else {
+      logger.debug(`Creating file ${file.name}`);
+      await createFile(branchName, file.name, file.contents, message);
+    }
+  }
+}
+
+// Internal branch operations
+
+// Creates a new branch with provided commit
+async function createBranch(branchName, ref = config.defaultBranch) {
+  await glGot.post(`projects/${config.repoName}/repository/branches`, {
+    body: {
+      branch_name: branchName,
+      ref,
+    },
+  });
+}
diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 0c99b61efcc56286cc93fd12cc4a0bbbe68a9f78..c00533c80fafd605a13141a87a5af2471a966e73 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -111,7 +111,7 @@ const options = [
     name: 'prBody',
     description: 'Pull Request body template',
     type: 'string',
-    default: 'This Pull Request updates dependency {{depName}} from version {{currentVersion}} to {{newVersion}}\n\n{{changelog}}',
+    default: 'This Pull Request updates dependency {{depName}} from version `{{currentVersion}}` to `{{newVersion}}`\n\n{{changelog}}',
     cli: false,
     env: false,
   },
diff --git a/lib/index.js b/lib/index.js
index 75eff27c657edd84b5caa30d64e6788b0a003c5f..eb80b49bd6f0f97f0ab3a41666f9155005872337 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -2,6 +2,7 @@ const stringify = require('json-stringify-pretty-compact');
 const logger = require('./logger');
 const configParser = require('./config');
 const githubApi = require('./api/github');
+const gitlabApi = require('./api/gitlab');
 const defaultsParser = require('./config/defaults');
 
 // Require main source
@@ -35,7 +36,9 @@ async function processRepo(repo) {
   const config = Object.assign({}, repo);
   if (config.platform === 'github') {
     api = githubApi;
-  } else { // Gitlab will be added here
+  } else if (config.platform === 'gitlab') {
+    api = gitlabApi;
+  } else {
     logger.error(`Unknown platform ${config.platform} for repository ${repo.repository}`);
     return;
   }
@@ -91,8 +94,7 @@ async function configureRepository(config) {
   const defaultConfig = defaultsParser.getConfig();
   delete defaultConfig.token;
   delete defaultConfig.repositories;
-  const defaultConfigString = `${stringify(defaultConfig)}\n`;
-  const prBody = `Welcome to [Renovate](https://keylocation.sg/our-tech/renovate)! Once you close this Pull Request, we will begin keeping your dependencies up-to-date via automated Pull Requests.
+  let prBody = `Welcome to [Renovate](https://keylocation.sg/our-tech/renovate)! Once you close this Pull Request, we will begin keeping your dependencies up-to-date via automated Pull Requests.
 
 #### Important!
 
@@ -100,9 +102,11 @@ You do not need to *merge* this Pull Request - renovate will begin even if it's
 In fact, you only need to add a \`renovate.json\` file to your repository if you wish to override any default settings. The file is included as part of this PR only in case you wish to change default settings before you start.
 If the default settings are all suitable for you, simply close this Pull Request unmerged and your first renovation will begin the next time the program is run.`;
 
-  if (config.platform === 'github') {
-    // Do nothing
+  if (config.platform === 'gitlab') {
+    defaultConfig.platform = 'gitlab';
+    prBody = prBody.replace(/Pull Request/g, 'Merge Request');
   }
+  const defaultConfigString = `${stringify(defaultConfig)}\n`;
   await api.commitFilesToBranch(
     'renovate/configure',
     [{
diff --git a/lib/worker.js b/lib/worker.js
index 5bebc01d1f555b39be1c64dcdf0c55c9276719d9..d9bc0dc125464a1d98e9bae54f6fbd6c948296e4 100644
--- a/lib/worker.js
+++ b/lib/worker.js
@@ -6,6 +6,7 @@ const cp = require('child_process');
 const tmp = require('tmp');
 const stringify = require('json-stringify-pretty-compact');
 const githubApi = require('./api/github');
+const gitlabApi = require('./api/gitlab');
 const handlebars = require('./helpers/handlebars');
 const versionsHelper = require('./helpers/versions');
 const packageJson = require('./helpers/package-json');
@@ -20,7 +21,10 @@ module.exports = renovate;
 async function renovate(repoName, packageFile, packageConfig) {
   if (packageConfig.platform === 'github') {
     api = githubApi;
-  } // Other platforms like Gitlab will go here
+  }
+  if (packageConfig.platform === 'gitlab') {
+    api = gitlabApi;
+  }
   // Initialize globals
   config = Object.assign({}, packageConfig);
   config.packageFile = packageFile;
diff --git a/package.json b/package.json
index cf7dcb987820a9d31e06c1c3e303220f45b0a76c..e3032a7408c298e707b0f98be9c9cb3cffdafb93 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
     "changelog": "dylang/changelog#v1.2.0",
     "commander": "2.9.0",
     "gh-got": "5.0.0",
+    "gl-got": "6.0.0",
     "got": "6.7.1",
     "handlebars": "4.0.6",
     "jest": "18.1.0",
diff --git a/readme.md b/readme.md
index 20a41e0bcf783e08bcd9d7f13a98649b74220e84..b0f48546eb9695657fb1eb39f9b1d36f38b480e9 100644
--- a/readme.md
+++ b/readme.md
@@ -9,6 +9,7 @@
 - Supports multiple major versions per-dependency at once
 - Configurable via file, environment, CLI, and `package.json`
 - Supports `yarn.lock` files
+- Supports GitHub and GitLab
 - Self-hosted
 
 ## Install
@@ -19,12 +20,14 @@ $ npm install -g renovate
 
 ## Authentication
 
-You need to select a GitHub user for `renovate` to assume the identity of. It's recommended that you use a dedicated "bot" account for this to avoid user confusion.
+You need to select a repository user for `renovate` to assume the identity of, and generate a Personal Access Token. It's recommended that you use a dedicated "bot" account for this to avoid user confusion.
 
-The script will need a GitHub Personal Access Token with "repo" permissions. You can find instructions for generating it here: https://help.github.com/articles/creating-an-access-token-for-command-line-use/
+You can find instructions for GitHub here (select "repo" permissions): https://help.github.com/articles/creating-an-access-token-for-command-line-use/
+
+You can find instructions for GitLab here: https://docs.gitlab.com/ee/api/README.html#personal-access-tokens
 
 This token needs to be configured via file, environment variable, or CLI. See [docs/configuration.md](docs/configuration.md) for details.
-The simplest way is to expose it as `GITHUB_TOKEN`.
+The simplest way is to expose it as `GITHUB_TOKEN` or `GITLAB_TOKEN`.
 
 ## Usage
 
@@ -61,7 +64,7 @@ $ node renovate --help
     $ renovate singapore/lint-condo singapore/package-test
 ```
 
-Note: The first time you run `renovate` on a repository, it will not upgrade any dependencies. Instead, it will create a PR called 'Configure Renovate' and commit a default `renovate.json` file to the repository. This PR can be close unmerged if the default settings are fine for you. Also, this behaviour can be disabled if you first disable the `onboarding` setting before running.
+Note: The first time you run `renovate` on a repository, it will not upgrade any dependencies. Instead, it will create a Pull Request (Merge Request if GitLab) called 'Configure Renovate' and commit a default `renovate.json` file to the repository. This PR can be close unmerged if the default settings are fine for you. Also, this behaviour can be disabled if you set the `onboarding` configuration option to `false` before running.
 
 ## Deployment
 
diff --git a/yarn.lock b/yarn.lock
index eaab692337a3ae2a5517a71dad381bf6d3b94317..14caf25497a8ca6e92971c70ff30bb280c037632 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1325,6 +1325,13 @@ gh-got@5.0.0:
     got "^6.2.0"
     is-plain-obj "^1.1.0"
 
+gl-got@6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/gl-got/-/gl-got-6.0.0.tgz#6abbaa8cd07464eb4064cdeb9fcacb5e7251e1b1"
+  dependencies:
+    got "^6.2.0"
+    is-plain-obj "^1.1.0"
+
 glob-base@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"