Last active
October 23, 2023 15:45
-
-
Save gamedevsam/c5fe6eb375528d8c0c12f3c87db5548b to your computer and use it in GitHub Desktop.
Script to deploy docker file to dokku (requires node 18 or greater + node-ssh and xid-js npm packages)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dotenv/config'; | |
import { NodeSSH } from 'node-ssh'; | |
import { mkdir } from 'node:fs/promises'; | |
import { parseArgs } from 'node:util'; | |
import { next as generate_xid } from 'xid-js'; | |
import { printDuration, spawnPromise } from './utilities/spawn_promise.mjs'; | |
const { values: argv } = parseArgs({ | |
options: { | |
// (required) name of app to deploy, defaults to app-dev | |
app_name: { | |
type: 'string', | |
default: 'app-dev', | |
short: 'a' | |
}, | |
// (optional) directory that will store locally created docker images | |
local_directory: { | |
type: 'string', | |
default: 'dist', | |
short: 'l' | |
}, | |
// (optional) directory of the remote ssh machine that will store uploaded docker images | |
remote_directory: { | |
type: 'string', | |
default: 'tmp/docker_images', | |
short: 'r' | |
} | |
} | |
}); | |
const ssh = new NodeSSH(); | |
const app_name = argv.app_name; | |
const tag = generate_xid(); | |
const dockerImageTag = `dokku/${app_name}:${tag}`; | |
async function sshCommand(cmd, options) { | |
const startTime = performance.now(); | |
await ssh.execCommand(cmd, { | |
onStdout: (chunk) => console.log(chunk.toString('utf8')), | |
onStderr: (chunk) => console.error(chunk.toString('utf8')), | |
...options | |
}); | |
console.log(`[deploy_dokku]: ${cmd} completed in ${printDuration(startTime)}`); | |
} | |
try { | |
await ssh.connect({ | |
host: process.env.SSH_HOST, | |
username: process.env.SSH_USERNAME, | |
password: process.env.SSH_PASSWORD, | |
privateKeyPath: process.env.SSH_PRIVATE_KEY_PATH, | |
passphrase: process.env.SSH_PRIVATE_KEY_PASSWORD | |
}); | |
// Build docker image | |
console.log(`[deploy_dokku]: Building docker image: '${app_name}:${tag}', please wait...`); | |
await spawnPromise(`docker image build --label=com.dokku.app-name=${app_name} --tag=${dockerImageTag} .`, { | |
outputPrefix: '[deploy_dokku]: ', | |
forwardParams: false | |
}); | |
// Save docker image to disk | |
console.log(`[deploy_dokku]: Creating archive from docker image, please wait...`); | |
await mkdir(argv.local_directory, { recursive: true }); | |
await spawnPromise(`docker image save ${dockerImageTag} | gzip > ${argv.local_directory}/${app_name}_${tag}.tar.gz`, { | |
outputPrefix: '[deploy_dokku]: ', | |
forwardParams: false | |
}); | |
// Upload docker image archive to remote | |
await sshCommand(`mkdir -p ${argv.remote_directory}`); | |
console.log(`[deploy_dokku]: Uploading '${app_name}_${tag}.tar.gz', please wait...`); | |
const startTime = performance.now(); | |
await ssh.putFiles([ | |
{ | |
local: `${process.cwd()}/${argv.local_directory}/${app_name}_${tag}.tar.gz`, | |
remote: `${argv.remote_directory}/${app_name}_${tag}.tar.gz` | |
} | |
]); | |
console.log(`[deploy_dokku]: Upload of '${app_name}_${tag}.tar.gz' completed in ${printDuration(startTime)}`); | |
// Unpack & load docker image on remote | |
await sshCommand(`docker load < ${app_name}_${tag}.tar.gz`, { cwd: argv.remote_directory }); | |
// Disable dokku builder before we deploy the pre-built docker image | |
await sshCommand(`dokku builder:set ${app_name} selected null`); | |
// Deploy docker image on remote | |
await sshCommand(`dokku git:from-image ${app_name} ${dockerImageTag}`); | |
} catch (error) { | |
console.error(error); | |
} finally { | |
// Post-deployment cleanup (local commands must occur before remote commands) | |
try { | |
// Local commands (before remote) | |
await spawnPromise(`rm ${argv.local_directory}/${app_name}_${tag}.tar.gz`); | |
// Remote commands (after local) | |
await sshCommand(`rm ${argv.remote_directory}/${app_name}_${tag}.tar.gz`); | |
// Ensure subsquent git based deployments work successfully | |
// https://github.com/dokku/dokku/issues/5963#issuecomment-1615836280 | |
await sshCommand(`dokku git:set ${app_name} source-image`); | |
// Restore dokku builder after we finish deploying the app | |
// https://dokku.com/docs/deployment/builders/builder-management/?h=builder#overriding-the-auto-selected-builder | |
await sshCommand(`dokku builder:set ${app_name} selected`); | |
} catch {} | |
ssh.dispose(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { spawn } from 'child_process'; | |
/** | |
* @typedef {import('child_process').SpawnOptionsWithoutStdio & { | |
* silent?: boolean, | |
* outputPrefix?: string, | |
* forwardParams?: boolean | |
* }} SpawnPromiseOptions | |
* */ | |
/** | |
* @param {number} startTime | |
*/ | |
export function printDuration(startTime) { | |
const elapsedMs = performance.now() - startTime; | |
if (elapsedMs < 60_000) { | |
return `${(elapsedMs / 1_000).toFixed(2)}s`; | |
} | |
const minutes = Math.floor(elapsedMs / 60000); | |
const seconds = ((elapsedMs % 60000) / 1000).toFixed(0); | |
return seconds === '60' ? '1m:00s' : `${minutes}m:${(Number(seconds) < 10 ? '0' : '') + seconds}s`; | |
} | |
/** | |
* @param {string} command | |
* @param {SpawnPromiseOptions} [options] | |
*/ | |
export const spawnPromise = (command, options) => | |
new Promise((resolve, reject) => { | |
/** @type {string} */ | |
let cmd; | |
if (options?.forwardParams !== false) { | |
const args = process.argv.slice(2); | |
cmd = `${command}${args.length > 0 ? ` ${args.join(' ')}` : ''}`; | |
} else { | |
cmd = command; | |
} | |
if (!options?.silent) { | |
console.log(`${options?.outputPrefix ?? ''}BEGIN: ${cmd}`); | |
} | |
const startTime = performance.now(); | |
spawn('sh', ['-c', cmd], { | |
stdio: 'inherit', | |
...options | |
}).on('close', (code) => { | |
if (code) { | |
reject(`${options?.outputPrefix ?? ''}'${cmd}'FAILED with code: ${code} (${printDuration(startTime)})`); | |
} else { | |
if (!options?.silent) { | |
console.log(`${options?.outputPrefix ?? ''}END: ${cmd} (${printDuration(startTime)})`); | |
} | |
resolve(code); | |
} | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment