I started using CapRover recently for running first party and third party web services. CapRover lets me set up apps with HTTPS enabled, certificates automatically issued by Let's Encrypt, and I can scale instances up or down any time, on a single machine or even across multiple machines.
As of version 1.6.1 I have not found any built-in functionality that supports more complex deployments, e.g. when you have multiple app instances and you need to migrate a database before deploying a new version of the app.
I thought about writing a pre-deploy script but I spent a few days exploring other solutions first, including trying other PaaS like Dokku, but after all I like the convenience of CapRover. Then I came over a comment in one of their issues on GitHub which gave me enough hints to get started on my own solution.
What my script does:
- Sets the instance count of the app to
0
to avoid the old version of the app from accessing the database during or after the migration. - Runs database migration by appending
--migrate-database
to the entrypoint specified in myDockerfile
. The app understands this. For your information, the app is an ASP.NET Core Web API app that uses Entity Framework Core. The new Docker container is also automatically removed. - Passes in all of the environment variables specified for the app and uses the same network so that the database server can be reached at
srv-captain--*
.
Next I'll have to think about how to back up the database before migration. This can however work well enough for rapid deployment to a test/staging environment.
var preDeployFunction = async function (captainAppObj, dockerUpdateObject) {
const DockerApi = require("./built/docker/DockerApi");
const api = new DockerApi.default();
const setServiceInstances = async (service, count) => {
const inspection = await service.inspect();
const updateObject = { ...inspection.Spec, Mode: { Replicated: { Replicas: count } }, version: inspection.Version.Index };
await service.update(updateObject);
};
const run = async args => {
const imageName = dockerUpdateObject.TaskTemplate.ContainerSpec.Image;
const env = captainAppObj.envVars.map(kv => kv.key + "=" + kv.value);
const config = { Env: env, HostConfig: { AutoRemove: true, NetworkMode: captainAppObj.networks[0] } };
const {output} = await api.dockerode.run(imageName, args, process.stdout, config);
if (output.StatusCode !== 0) {
throw new Error(`Failed to run image ${imageName} with args ${args} (status code ${output.StatusCode}).`);
}
};
const service = api.dockerode.getService(dockerUpdateObject.Name);
await setServiceInstances(service, 0);
await run(["--migrate-database"]);
dockerUpdateObject.version = (await service.inspect()).Version.Index;
return dockerUpdateObject;
};