diff --git a/CHANGELOG.md b/CHANGELOG.md
index 601c5315..832239a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file
 automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
 This project adheres to [Semantic Versioning](http://semver.org/).
 
+## v7.4.0 - 2018-05-10
+
+* Add push command which starts a build on remote resin servers #868 [Cameron Diver]
+
 ## v7.3.8 - 2018-05-03
 
 * Catch require errors and provide helpful instructions #874 [Tim Perry]
diff --git a/lib/actions/index.coffee b/lib/actions/index.coffee
index b6303e11..37468209 100644
--- a/lib/actions/index.coffee
+++ b/lib/actions/index.coffee
@@ -37,3 +37,4 @@ module.exports =
 	deploy: require('./deploy')
 	util: require('./util')
 	preload: require('./preload')
+	push: require('./push')
diff --git a/lib/actions/push.ts b/lib/actions/push.ts
new file mode 100644
index 00000000..5f7fac11
--- /dev/null
+++ b/lib/actions/push.ts
@@ -0,0 +1,142 @@
+/*
+Copyright 2016-2017 Resin.io
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { CommandDefinition } from 'capitano';
+import { ResinSDK } from 'resin-sdk';
+import { stripIndent } from 'common-tags';
+
+async function getAppOwner(sdk: ResinSDK, appName: string) {
+	const {
+		exitWithExpectedError,
+		selectFromList,
+	} = await import('../utils/patterns');
+	const _ = await import('lodash');
+
+	const applications = await sdk.models.application.getAll({
+		$expand: {
+			user: {
+				$select: ['username'],
+			},
+		},
+		$filter: {
+			app_name: appName,
+		},
+		$select: ['id'],
+	});
+
+	if (applications == null || applications.length === 0) {
+		exitWithExpectedError(
+			stripIndent`
+			No applications found with name: ${appName}.
+
+			This could mean that the application does not exist, or you do
+			not have the permissions required to access it.`,
+		);
+	}
+
+	if (applications.length === 1) {
+		return _.get(applications, '[0].user[0].username');
+	}
+
+	// If we got more than one application with the same name it means that the
+	// user has access to a collab app with the same name as a personal app. We
+	// present a list to the user which shows the fully qualified application
+	// name (user/appname) and allows them to select
+	const entries = _.map(applications, app => {
+		const username = _.get(app, 'user[0].username');
+		return {
+			name: `${username}/${appName}`,
+			extra: username,
+		};
+	});
+
+	const selected = await selectFromList(
+		`${
+			entries.length
+		} applications found with that name, please select the application you would like to push to`,
+		entries,
+	);
+
+	return selected.extra;
+}
+
+export const push: CommandDefinition<
+	{
+		application: string;
+	},
+	{
+		source: string;
+	}
+> = {
+	signature: 'push <application>',
+	description: 'Start a remote build on the resin.io cloud build servers',
+	help: stripIndent`
+		This command can be used to start a build on the remote
+		resin.io cloud builders. The given source directory will be sent to the
+		resin.io builder, and the build will proceed. This can be used as a drop-in
+		replacement for git push to deploy.
+
+		Examples:
+
+			$ resin push myApp
+			$ resin push myApp --source <source directory>
+			$ resin push myApp -s <source directory>
+	`,
+	permission: 'user',
+	options: [
+		{
+			signature: 'source',
+			alias: 's',
+			description:
+				'The source that should be sent to the resin builder to be built (defaults to the current directory)',
+			parameter: 'source',
+		},
+	],
+	async action(params, options, done) {
+		const sdk = (await import('resin-sdk')).fromSharedOptions();
+		const Bluebird = await import('bluebird');
+		const remote = await import('../utils/remote-build');
+		const { exitWithExpectedError } = await import('../utils/patterns');
+
+		const app: string | null = params.application;
+		if (app == null) {
+			exitWithExpectedError('You must specify an application');
+		}
+
+		const source = options.source || '.';
+		if (process.env.DEBUG) {
+			console.log(`[debug] Using ${source} as build source`);
+		}
+
+		Bluebird.join(
+			sdk.auth.getToken(),
+			sdk.settings.get('resinUrl'),
+			getAppOwner(sdk, app),
+			(token, baseUrl, owner) => {
+				const args = {
+					app,
+					owner,
+					source,
+					auth: token,
+					baseUrl,
+					sdk,
+				};
+
+				return remote.startRemoteBuild(args);
+			},
+		).nodeify(done);
+	},
+};
diff --git a/lib/app.coffee b/lib/app.coffee
index 4d1f42b6..3d7b3245 100644
--- a/lib/app.coffee
+++ b/lib/app.coffee
@@ -212,6 +212,9 @@ capitano.command(actions.internal.osInit)
 capitano.command(actions.build)
 capitano.command(actions.deploy)
 
+#------------ Push/remote builds -------
+capitano.command(actions.push.push)
+
 update.notify()
 
 cli = capitano.parse(process.argv)
diff --git a/lib/utils/compose.coffee b/lib/utils/compose.coffee
index 3a0c24a3..a45ff51d 100644
--- a/lib/utils/compose.coffee
+++ b/lib/utils/compose.coffee
@@ -102,7 +102,7 @@ toPosixPath = (systemPath) ->
 	path = require('path')
 	systemPath.replace(new RegExp('\\' + path.sep, 'g'), '/')
 
-tarDirectory = (dir) ->
+exports.tarDirectory = tarDirectory = (dir) ->
 	tar = require('tar-stream')
 	klaw = require('klaw')
 	path = require('path')
diff --git a/lib/utils/compose.d.ts b/lib/utils/compose.d.ts
new file mode 100644
index 00000000..3b251828
--- /dev/null
+++ b/lib/utils/compose.d.ts
@@ -0,0 +1,3 @@
+import * as Stream from 'stream';
+
+export function tarDirectory(source: string): Promise<Stream.Readable>;
diff --git a/lib/utils/patterns.ts b/lib/utils/patterns.ts
index cd4fc11d..e805929b 100644
--- a/lib/utils/patterns.ts
+++ b/lib/utils/patterns.ts
@@ -23,6 +23,11 @@ import chalk from 'chalk';
 import validation = require('./validation');
 import messages = require('./messages');
 
+export interface ListSelectionEntry {
+	name: string;
+	extra: any;
+}
+
 export function authenticate(options: {}): Promise<void> {
 	return form
 		.run(
@@ -245,6 +250,20 @@ export function inferOrSelectDevice(preferredUuid: string) {
 		});
 }
 
+export function selectFromList(
+	message: string,
+	selections: ListSelectionEntry[],
+): Promise<ListSelectionEntry> {
+	return form.ask({
+		message,
+		type: 'list',
+		choices: _.map(selections, s => ({
+			name: s.name,
+			value: s,
+		})),
+	});
+}
+
 export function printErrorMessage(message: string) {
 	console.error(chalk.red(message));
 	console.error(chalk.red(`\n${messages.getHelp}\n`));
diff --git a/lib/utils/remote-build.ts b/lib/utils/remote-build.ts
new file mode 100644
index 00000000..0d4624ab
--- /dev/null
+++ b/lib/utils/remote-build.ts
@@ -0,0 +1,215 @@
+/*
+Copyright 2016-2017 Resin.io
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import * as JSONStream from 'JSONStream';
+import * as request from 'request';
+import { ResinSDK } from 'resin-sdk';
+import * as Stream from 'stream';
+
+import { tarDirectory } from './compose';
+
+const DEBUG_MODE = !!process.env.DEBUG;
+
+const CURSOR_METADATA_REGEX = /([a-z]+)([0-9]+)?/;
+const TRIM_REGEX = /\n+$/;
+
+export interface RemoteBuild {
+	app: string;
+	owner: string;
+	source: string;
+	auth: string;
+	baseUrl: string;
+
+	sdk: ResinSDK;
+
+	// For internal use
+	releaseId?: number;
+}
+
+interface BuilderMessage {
+	message: string;
+	type?: string;
+	replace?: boolean;
+	// These will be set when the type === 'metadata'
+	resource?: string;
+	value?: string;
+}
+
+async function getBuilderEndpoint(
+	baseUrl: string,
+	owner: string,
+	app: string,
+): Promise<string> {
+	const querystring = await import('querystring');
+	const args = querystring.stringify({ owner, app });
+	return `https://builder.${baseUrl}/v3/build?${args}`;
+}
+
+export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
+	const Bluebird = await import('bluebird');
+
+	return new Bluebird(async (resolve, reject) => {
+		const stream = await getRequestStream(build);
+
+		// Special windows handling (win64 also reports win32)
+		if (process.platform === 'win32') {
+			const readline = (await import('readline')).createInterface({
+				input: process.stdin,
+				output: process.stdout,
+			});
+
+			readline.on('SIGINT', () => process.emit('SIGINT'));
+		}
+
+		// Setup interrupt handlers so we can cancel the build if the user presses
+		// ctrl+c
+
+		// This is necessary because the `exit-hook` module is used by several
+		// dependencies, and will exit without calling the following handler.
+		// Once https://github.com/resin-io/resin-cli/issues/867 has been solved,
+		// we are free to (and definitely should) remove the below line
+		process.removeAllListeners('SIGINT');
+		process.on('SIGINT', () => {
+			process.stderr.write('Received SIGINT, cleaning up. Please wait.\n');
+			cancelBuildIfNecessary(build).then(() => {
+				stream.end();
+				process.exit(130);
+			});
+		});
+
+		stream.on('data', getBuilderMessageHandler(build));
+		stream.on('end', resolve);
+		stream.on('error', reject);
+	}).return();
+}
+
+async function handleBuilderMetadata(obj: BuilderMessage, build: RemoteBuild) {
+	const { stripIndent } = await import('common-tags');
+
+	switch (obj.resource) {
+		case 'cursor':
+			const readline = await import('readline');
+
+			if (obj.value == null) {
+				return;
+			}
+
+			const match = obj.value.match(CURSOR_METADATA_REGEX);
+
+			if (!match) {
+				// FIXME: Make this error nicer.
+				console.log(
+					stripIndent`
+					Warning: ignoring unknown builder command. You may experience
+					odd build output. Maybe you need to update resin-cli?`,
+				);
+				return;
+			}
+
+			const value = match[1];
+			const amount = match[2] || 1;
+
+			switch (value) {
+				case 'erase':
+					readline.clearLine(process.stdout, 0);
+					process.stdout.write('\r');
+					break;
+				case 'up':
+					readline.moveCursor(process.stdout, 0, -amount);
+					break;
+				case 'down':
+					readline.moveCursor(process.stdout, 0, amount);
+					break;
+			}
+
+			break;
+		case 'buildLogId':
+			// The name of this resource is slightly dated, but this is the release
+			// id from the API. We need to save this so that if the user ctrl+c's the
+			// build we can cancel it on the API.
+			build.releaseId = parseInt(obj.value!, 10);
+			break;
+	}
+}
+
+function getBuilderMessageHandler(
+	build: RemoteBuild,
+): (obj: BuilderMessage) => Promise<void> {
+	return async (obj: BuilderMessage) => {
+		if (DEBUG_MODE) {
+			console.log(`[debug] handling message: ${JSON.stringify(obj)}`);
+		}
+		if (obj.type != null && obj.type === 'metadata') {
+			return handleBuilderMetadata(obj, build);
+		}
+		if (obj.message) {
+			const readline = await import('readline');
+			readline.clearLine(process.stdout, 0);
+
+			const message = obj.message.replace(TRIM_REGEX, '');
+			if (obj.replace) {
+				process.stdout.write(`\r${message}`);
+			} else {
+				process.stdout.write(`\r${message}\n`);
+			}
+		}
+	};
+}
+
+async function cancelBuildIfNecessary(build: RemoteBuild): Promise<void> {
+	if (build.releaseId != null) {
+		await build.sdk.pine.patch({
+			resource: 'release',
+			id: build.releaseId,
+			body: {
+				status: 'cancelled',
+				end_timestamp: Date.now(),
+			},
+		});
+	}
+}
+
+async function getRequestStream(build: RemoteBuild): Promise<Stream.Duplex> {
+	const path = await import('path');
+	const visuals = await import('resin-cli-visuals');
+
+	const tarSpinner = new visuals.Spinner('Packaging the project source...');
+	tarSpinner.start();
+	// Tar the directory so that we can send it to the builder
+	const tarStream = await tarDirectory(path.resolve(build.source));
+	tarSpinner.stop();
+
+	if (DEBUG_MODE) {
+		console.log('[debug] Opening builder connection');
+	}
+	const post = request.post({
+		url: await getBuilderEndpoint(build.baseUrl, build.owner, build.app),
+		auth: {
+			bearer: build.auth,
+		},
+	});
+
+	const uploadSpinner = new visuals.Spinner(
+		'Uploading source package to resin cloud',
+	);
+	uploadSpinner.start();
+
+	tarStream.pipe(post);
+
+	const parseStream = post.pipe(JSONStream.parse('*'));
+	parseStream.on('data', () => uploadSpinner.stop());
+	return parseStream as Stream.Duplex;
+}
diff --git a/package.json b/package.json
index a24567fb..acfa115a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "resin-cli",
-  "version": "7.3.8",
+  "version": "7.4.0",
   "description": "The official resin.io CLI tool",
   "main": "./build/actions/index.js",
   "homepage": "https://github.com/resin-io/resin-cli",
@@ -89,6 +89,7 @@
   },
   "dependencies": {
     "@resin.io/valid-email": "^0.1.0",
+    "@types/stream-to-promise": "^2.2.0",
     "ansi-escapes": "^2.0.0",
     "any-promise": "^1.3.0",
     "archiver": "^2.1.0",
@@ -117,6 +118,7 @@
     "inquirer": "^3.1.1",
     "is-root": "^1.0.0",
     "js-yaml": "^3.10.0",
+    "JSONStream": "^1.0.3",
     "klaw": "^1.3.1",
     "lodash": "^4.17.4",
     "mixpanel": "^0.4.0",
@@ -146,7 +148,7 @@
     "resin-multibuild": "^0.5.1",
     "resin-preload": "^6.2.0",
     "resin-release": "^1.2.0",
-    "resin-sdk": "9.0.0-beta17",
+    "resin-sdk": "9.0.0-beta18",
     "resin-sdk-preconfigured": "^6.9.0",
     "resin-settings-client": "^3.6.1",
     "resin-stream-logger": "^0.1.0",
diff --git a/typings/JSONStream.d.ts b/typings/JSONStream.d.ts
new file mode 100644
index 00000000..0c9909f7
--- /dev/null
+++ b/typings/JSONStream.d.ts
@@ -0,0 +1,53 @@
+// These are the DefinitelyTyped typings for JSONStream, but because of this
+// mismatch in case of jsonstream and JSONStream, it is necessary to include
+// them this way, with an upper case module declaration. They have also
+// been slightly edited to remove the extra `declare` keyworks (which are
+// not necessary or accepted inside a `declare module '...' {` block)
+declare module 'JSONStream' {
+	// Type definitions for JSONStream v0.8.0
+	// Project: https://github.com/dominictarr/JSONStream
+	// Definitions by: Bart van der Schoor <https://github.com/Bartvds>
+	// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
+
+	/// <reference types="node" />
+
+	export interface Options {
+		recurse: boolean;
+	}
+
+	export function parse(pattern: any): NodeJS.ReadWriteStream;
+	export function parse(patterns: any[]): NodeJS.ReadWriteStream;
+
+	/**
+	 * Create a writable stream.
+	 * you may pass in custom open, close, and seperator strings. But, by default,
+	 * JSONStream.stringify() will create an array,
+	 * (with default options open='[\n', sep='\n,\n', close='\n]\n')
+	 */
+	export function stringify(): NodeJS.ReadWriteStream;
+
+	/** If you call JSONStream.stringify(false) the elements will only be seperated by a newline. */
+	export function stringify(
+		newlineOnly: NewlineOnlyIndicator,
+	): NodeJS.ReadWriteStream;
+	type NewlineOnlyIndicator = false;
+
+	/**
+	 * Create a writable stream.
+	 * you may pass in custom open, close, and seperator strings. But, by default,
+	 * JSONStream.stringify() will create an array,
+	 * (with default options open='[\n', sep='\n,\n', close='\n]\n')
+	 */
+	export function stringify(
+		open: string,
+		sep: string,
+		close: string,
+	): NodeJS.ReadWriteStream;
+
+	export function stringifyObject(): NodeJS.ReadWriteStream;
+	export function stringifyObject(
+		open: string,
+		sep: string,
+		close: string,
+	): NodeJS.ReadWriteStream;
+}