Files
dokploy/packages/server/src/utils/databases/mongo.ts
Mauricio Siu e944603f99 fix: use stop-first update order for all database services (#4560)
Docker Swarm's default start-first update order causes new database
containers to fail with 'DBPathInUse' because two containers compete
for the same data volume simultaneously. Docker then rolls back the
update, silently reverting any env var or config changes.

Using stop-first ensures the old container is stopped before the new
one starts, preventing volume lock conflicts across all database types.

Fixes #4550
2026-06-06 14:49:24 -06:00

192 lines
4.1 KiB
TypeScript

import type { InferResultType } from "@dokploy/server/types/with";
import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
generateBindMounts,
generateConfigContainer,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { getRemoteDocker } from "../servers/remote-docker";
export type MongoNested = InferResultType<
"mongo",
{ mounts: true; environment: { with: { project: true } } }
>;
export const buildMongo = async (mongo: MongoNested) => {
const {
appName,
env,
externalPort,
dockerImage,
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
databaseUser,
databasePassword,
command,
args,
mounts,
replicaSets,
} = mongo;
const startupScript = `
#!/bin/bash
${
replicaSets
? `
mongod --port 27017 --replSet rs0 --bind_ip_all &
MONGOD_PID=$!
# Wait for MongoDB to be ready
while ! mongosh --eval "db.adminCommand('ping')" > /dev/null 2>&1; do
sleep 2
done
# Check if replica set is already initialized
REPLICA_STATUS=$(mongosh --quiet --eval "rs.status().ok || 0")
if [ "$REPLICA_STATUS" != "1" ]; then
echo "Initializing replica set..."
mongosh --eval '
rs.initiate({
_id: "rs0",
members: [{ _id: 0, host: "${appName}:27017", priority: 1 }]
});
// Wait for the replica set to initialize
while (!rs.isMaster().ismaster) {
sleep(1000);
}
// Create root user after replica set is initialized and we are primary
db.getSiblingDB("admin").createUser({
user: "${databaseUser}",
pwd: "${databasePassword}",
roles: ["root"]
});
'
else
echo "Replica set already initialized."
fi
`
: ""
}
${command ?? "wait $MONGOD_PID"}`;
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME="${databaseUser}"\nMONGO_INITDB_ROOT_PASSWORD="${databasePassword}"${replicaSets ? "\nMONGO_INITDB_DATABASE=admin" : ""}${
env ? `\n${env}` : ""
}`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
StopGracePeriod,
EndpointSpec,
Ulimits,
} = generateConfigContainer(mongo);
const resources = calculateResources({
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
});
const envVariables = prepareEnvironmentVariables(
defaultMongoEnv,
mongo.environment.project.env,
mongo.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mongo);
const docker = await getRemoteDocker(mongo.serverId);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {
ContainerSpec: {
HealthCheck,
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
...(replicaSets
? {
Command: ["/bin/bash"],
Args: ["-c", startupScript],
}
: {}),
...(command &&
!replicaSets && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 &&
!replicaSets && {
Args: args,
}),
...(Ulimits && { Ulimits }),
Labels,
},
Networks,
RestartPolicy,
Placement,
Resources: {
...resources,
},
},
Mode,
RollbackConfig,
EndpointSpec: EndpointSpec
? EndpointSpec
: {
Mode: "dnsrr" as const,
Ports: externalPort
? [
{
Protocol: "tcp" as const,
TargetPort: 27017,
PublishedPort: externalPort,
PublishMode: "host" as const,
},
]
: [],
},
UpdateConfig: mongo.updateConfigSwarm ?? {
Parallelism: 1,
Order: "stop-first" as const,
FailureAction: "rollback" as const,
},
};
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch {
await docker.createService(settings);
}
};