Building a custom ERPNext Docker image with your apps

A custom ERPNext image bakes ERPNext, Frappe HR, India Compliance and your own apps into one versioned artifact your team can build, test and roll back. This page explains the build options, the sequence and what to confirm before you commit.

On this page

When a custom ERPNext Docker image is worth building

If you run ERPNext in containers and need Frappe HR, India Compliance or in-house apps in every environment, a custom image is usually worth building. Installing apps by hand inside running containers makes each environment slightly different, and a rebuild or a failed upgrade then becomes guesswork. One image that bundles ERPNext and the exact apps you use gives you repeatable deploys, a pinned version you can point to in a change record, and a rollback path: redeploy the previous tag and the containers return to the state you tested. The stock images are enough when you run only ERPNext core, your team does not maintain custom code, and you accept the apps as they ship.

The starting point for production is the frappe_docker repository, which provides production Docker images and Compose files for ERPNext and other Frappe apps. It defines the services you will operate: MariaDB, Redis, background workers, the scheduler, Socket.io and Nginx. Its single-file demo setup is marked for short-lived evaluation only, so do not use it as your production baseline. Confirm in the repository which build files and Compose variants match your ERPNext major version before you copy any of them, because the services and environment settings you need differ between releases.

Before you commit to a custom image, decide who owns the build pipeline, who approves app versions, and how often you will rebuild for ERPNext minor releases. Both supported major versions receive frequent minor releases, so an image you never rebuild will drift from security fixes. Confirm with your Frappe partner or internal team which of your apps are compatible with your chosen ERPNext version, and whether any app needs a Frappe Cloud support plan for the warranty coverage that Frappe describes in its supported versions policy.

Custom image or layered image: which build approach fits

A single custom image bakes ERPNext, Frappe HR, India Compliance and your own apps into one artifact, built from one apps.json definition. Every server runs the same image, so the set of code is fixed at build time and a deployment is one tag to roll forward.

  • One artifact per release: The build output contains every app at the commit you selected, so the deployed state is easy to describe.
  • Build-time coupling: A change to any one app, including a small custom app, triggers a full rebuild of the image.
  • Private code access: Credentials for private repositories are needed only during the build. Use build secrets so they do not persist in image layers.
  • Larger images and slower pulls: Each release carries every app, so registry storage and pull times grow with the app list.

Before choosing this model, confirm with your registry and platform teams that full rebuilds for every app change, and the storage and pull times they bring, are acceptable for your release cadence.

How apps.json defines what goes into the image

The apps.json file is the list the image build reads to decide what goes inside it. Each entry names an app, the Git URL it is fetched from, and the branch or tag to check out. The build clones those repositories, installs them into the bench, and bakes the result into one image, so anything missing from the file will not be in the image, and anything pinned loosely will change under you on the next build. In practice, the file is the bill of materials for the image, and it should be reviewed like any other dependency manifest. Keep it in version control next to the Dockerfile so a change to an app source or ref shows up in a diff and goes through the same review as application code.

The most important check is version alignment. ERPNext, Frappe HR and India Compliance each declare which Frappe Framework major version they run on, and a mismatch will fail during install or break at runtime. Each branch or tag in apps.json must therefore target the same Frappe version as the core. Confirm this against the releases page for each app and against Frappe's supported versions page, which lists the Frappe and ERPNext versions currently in support and their end-of-life dates. Version 14 reached end of life on January 31, 2026, so an image built on that line carries no upstream support, and this is a decision to make before any build starts. Record the chosen version alongside the apps.json entries so that auditors and future maintainers can see which combination was tested.

Configuration options for the image build

This table compares the main decisions you make before building an ERPNext image with your apps, including app source, pinning, base image, secrets and layering. Read each row as a choice with a condition under which it fits and a cost you accept by choosing it. Most teams end up pinning every input to an exact commit and rebuilding deliberately, because that trade-off keeps builds reproducible for audit and rollback.

DecisionOption and when it fitsTrade-off to accept
App sourcePublic repository for open apps such as Frappe HR or India Compliance, when you do not change their codeYou depend on upstream branches and release cadence, so review every change before rebuilding
App sourcePrivate fork for apps you have customized, or for code that must stay inside your networkYou own merges from upstream, security patches and test coverage for the fork
Version referenceTag or branch name, when you want minor releases picked up on each rebuildTwo builds from the same configuration can produce different images, which complicates rollback
Version referenceExact commit hash, when you need reproducible builds for change records and incident reviewYou must update the hash deliberately and retest, so upgrades take more planning
Base imageOfficial Frappe-maintained base image, when you want the runtime the project tests againstYou still need to track its updates and confirm they match your ERPNext major version
Base imageYour own hardened base image, when your security policy requires approved operating system layersYou own patching of the base and must rebuild the stack whenever it changes
Build secretsPrivate repository access passed as a build secret, so tokens do not persist in image layersYour CI system must handle secret injection correctly, and a misconfigured step can still leak credentials
Build secretsNo secrets at build time, when all sources are public or pulled at deploy timeRuntime configuration then carries more of the load, and images cannot fetch private code
Image layoutSingle image with all apps baked in, when each release is tested as one unitAny app change forces a full rebuild and a new deployment of every service
Image layoutLayered build on a shared base, when apps change at different rates and you want faster rebuildsMore moving parts to version together, and the layer order must match your dependency graph

Build sequence from apps.json to a tested image

This sequence turns a list of Frappe apps into one versioned image that has been installed and tested against a copy of your data before anyone promotes it. Each step ends with a check you can record, so the team can show which versions were built, tested and deployed.

  1. Confirm the Frappe and ERPNext version pair

    Start by choosing the Frappe Framework and ERPNext versions from the supported list on Frappe's supported versions page, and confirm that every companion app you plan to include has a release branch matching that Frappe major version. Your team or DeployOpen records the pair in the project decision log before any file is edited. The step is done when the pair is written down, checked against the list, and the owner of the decision has signed it off. Confirm with Frappe which support plan, if any, applies to your version choice.

  2. Write apps.json with pinned sources

    Create the apps.json file listing each app by its Git URL and the exact branch or tag to build from, including ERPNext, Frappe HR or other Frappe apps, and any in-house apps. Keep this file in version control next to the build pipeline so every change is reviewed. The output is a committed apps.json where each entry resolves to a specific reference rather than a moving default. Confirm that any private repository is reachable from the build environment without embedding credentials in the file.

  3. Build the image with frappe_docker tooling

    Run the image build using the build tooling in the frappe_docker repository, passing the apps.json content as a build argument so the build pulls exactly the sources you listed. Supply any private repository token through a build secret rather than an argument or environment variable, so it does not persist in image layers. The build is complete when the image is produced without errors and the build log shows each app cloned at the reference you set. Record the build log and the image digest with the build record.

  4. Verify installed app versions inside the container

    Start a container from the new image and check which apps and versions are present, for example by listing installed apps and reading the version of each through the bench tooling inside the container. Compare the output with the apps.json entries and the version pair from the first step. The check passes only when every app appears with the expected reference and no unexpected app is present. Store this output with the build record, because it is the evidence auditors and your own change reviewers will ask for.

  5. Test against a staging copy of the database

    Run the image against a staging environment restored from a recent copy of production data, with the site migrated using the new image. Run the migration, then exercise the workflows your business depends on, such as posting invoices, running stock reconciliation and generating key reports. Confirm that custom fields, scripts and integrations behave as expected after migration. The step is done when migration finishes without errors and your agreed test cases pass, with results recorded. Check the release notes for the chosen versions for known breaking changes that affect your apps.

  6. Tag the image with an immutable version

    Tag the tested image with a version identifier that is never reused, such as a combination of the release name and the source revisions or a build number, and record the image digest alongside it. Avoid relying on a floating tag like latest for production deployments. The output is a tag and digest pair that can always be traced back to the apps.json file and build log. Confirm that your registry settings prevent overwriting existing tags, or that your pipeline enforces this.

  7. Promote the tested image to production

    Promote the image by deploying the same digest that passed staging, not a rebuild, so the artifact in production is the one you tested. Keep the previous image tag available so the team can roll back by redeploying it. The step is complete when production reports the expected app versions, the scheduler and workers are running, and a smoke test of key pages succeeds. Record the promotion, the person who approved it, and the rollback tag in the change log.

Build secrets, private repositories and registry access

A custom ERPNext image often pulls private app repositories and pushes to a private registry, so the build touches more credentials than the running system does. The cards below cover where those credentials live during the build, how CI accounts are scoped, how images are checked, and how the build stays apart from production.

Private repository tokens at build time

Apps in a private Git host need a token or deploy key when the image build clones them. Pass the credential as a BuildKit secret mounted for a single RUN step, not as a --build-arg or ENV line, because build arguments and environment variables are written into image metadata and can be read back with docker history. Check that your build tooling supports secret mounts and that no layer contains the token after the build finishes.

Scoping the apps.json source

The apps.json file lists each app's Git URL and branch or tag, so it often contains private URLs even when it holds no secret. Keep it in version control with pinned refs, and avoid embedding tokens in the URL itself. Ask your team whether the file is encoded as a build secret or read from the repository, since a URL with a credential is a leak even in a private repo.

Scoped registry credentials for CI

The CI job that pushes images needs write access to one registry namespace, and nothing else. Create a dedicated robot or service account per repository, grant push and pull rights only where needed, and set an expiry or rotation schedule. Separate the pull credential used by the production cluster from the push credential used by the build, so a leak of one does not grant the other.

Image scanning before promotion

Scan every built image for known vulnerabilities in the operating system packages, Python dependencies and bundled JavaScript before it is tagged for deployment. Also scan for secrets left in layers or metadata, since a token that slipped into one layer stays in the image even if a later step deletes it. Decide who reviews findings and which severities block a release, and document that decision before the first production build.

Separating build and production secrets

Build-time credentials for Git and the registry should not be the same values that hold the ERPNext database password, encryption key or mail credentials. Store runtime secrets in the deployment platform, outside the image, and let the build runner see only what compiling the image requires. Confirm with your CI platform how it masks secrets in logs, who can read job variables, and whether forked or pull-request builds can access them.

Confirming your CI platform's behavior

Secret handling differs between CI platforms, including how variables are masked, which jobs inherit them, and whether build caches retain intermediate layers. Ask the platform vendor for its documented behavior on masked variables, cache scope and runner isolation, and test with a dummy token that a build log and the pushed image contain no trace of it. Record the result in the build runbook so the check repeats after any pipeline change.

Running image builds in CI without drifting from production

Treat the image build as a release pipeline, not a manual step. The CI job should run on every change to apps.json, to any app repository you pin in it, and to the Dockerfile or Compose files that define how the image is assembled. Each run builds the image, tags it with an immutable identifier such as the commit SHA of your configuration repository plus the build number, and pushes it to your registry. Avoid relying on a mutable tag such as latest or a branch name for anything that reaches production, because a later build can silently replace what a running environment believes it is using. Record the result in a build manifest stored alongside the tag: the ERPNext and Frappe versions, the exact commit of every custom or third-party app, and the base image digest. Without that record, the question of which app versions a given tag contains becomes a forensic exercise during an incident or an audit.

Before a tag is promoted, the pipeline should run smoke tests against a throwaway environment built from that image. At minimum, confirm that the site can be created, that migrations complete without errors, that the web, worker and scheduler containers start, and that each installed app loads and its key DocTypes respond through the API. Add a check that the manifest matches what is actually installed inside the image, because a mismatch usually means a cached layer was reused. Run the same tests against the staging environment before production. Ask your build engineer to show where these results are stored and who can approve promotion, since the approval step is where drift is either caught or accepted.

Scheduled rebuilds deserve a separate rule. Upstream ERPNext and Frappe publish frequent minor releases, and the supported-version page lists the release lines you can target. A nightly or weekly rebuild that pulls the latest upstream minor release will produce a new image without any change on your side, so it is effectively a dependency upgrade. Each rebuild carries a cost: release notes to read, a regression run in staging, and a possible migration that must be planned around your change window. Do not let scheduled jobs push to production automatically. Instead, have them open a review request that lists the new versions and the changelog, then require a named owner to run the smoke tests, accept the diff and schedule the rollout. Confirm with your vendor or Frappe support plan what coverage applies to the version you run, since that determines how long you can stay on a given minor release before the review becomes mandatory.

Limits and what to confirm in your environment

A custom image bundles ERPNext, Frappe and every app into one artifact, so a mistake in one component reaches every site that runs it. The cards below cover the checks that carry the most risk: version compatibility, script execution policy, app licensing, the demo setup boundary and the capacity of your build and registry infrastructure.

App and Frappe version compatibility

Each third-party app in apps.json must declare support for the same major Frappe version as ERPNext in the image. Check the app's branch or tag against the version you pin, and confirm it installs cleanly on a staging site before the image reaches production. Apps that lag behind a release can break on migrate, and a broken app blocks the whole image rollout, not only its own features. Pin the version of every app in the build so a rebuild next month does not pull different code.

Server Scripts enablement policy

Server Scripts run Python on document events and as API endpoints, so enabling them changes what code runs inside your database-backed application. From version 15 they are disabled by default and turned on with the server_script_enabled setting. Confirm with your security team whether your deployment allows them at all, and whether an image that bundles custom scripts should ship with them switched on. Shared hosting environments may not permit them, so a script-based customization that works on your server may not run on a provider's platform.

Licensing of private apps

ERPNext is GPL-3.0 and the Frappe Framework is MIT, but a private app you build or buy carries its own license terms. Ask who owns the code, which license it uses, and whether distributing an image that contains it to other entities, such as subsidiaries, contractors or a hosting provider, is permitted. A GPL-3.0 dependency can require source availability for distributed copies, which may not fit a closed commercial app. Have legal review the license of every app baked into the image before it leaves your network.

The frappe_docker demo boundary

The frappe_docker repository includes a single-file demo setup that its own documentation marks for short-lived evaluation only. It bundles convenience defaults that are unsuitable for production, such as the services sharing one host and no planned backup or recovery path. Build your production image from the same repository's production images and Compose files, then set your own database credentials, secrets and volumes. Do not promote an image or stack that started as the demo without reviewing each service.

Build time, image size and registry capacity

Build duration depends on the number of apps, the dependency tree of each one, the build host's CPU and network speed, and whether layer caching works between runs. Image size grows with every app and with the assets each app compiles, and each rebuild pushes new layers to your registry. Measure a full build and a cached rebuild on your own CI runners, record the final image size, and confirm your registry storage quota and retention policy can hold several tagged versions. These numbers come from your infrastructure, so ask your team to test them before committing to a release cadence.

Build secrets and credential handling

A build that clones private repositories needs a token, and that token must never be written into an image layer where anyone who pulls the image can extract it. Use your CI system's secret store with build secrets rather than build arguments, which remain visible in image history. Confirm that the registry is private, that pull access follows your access model, and that tokens rotate on a schedule your security team has set. Check the image history for leaked values before publishing any tag.

Questions teams ask before building a custom ERPNext image

Can our team build a custom ERPNext Docker image without outside help?

Yes, a team with Linux, Docker and Python skills can build one, and self-hosting ERPNext is free under the GPL-3.0 license. The work is less about writing the Dockerfile and more about keeping the app list, branches and versions aligned across ERPNext, Frappe and each custom app, then rebuilding and testing on every change. Ask whether your engineers have already run a Frappe bench and a CI pipeline that builds images, because those two skills decide how long the first build takes and how often it breaks. The frappe_docker repository is the reference point for production images and Compose files, and your team should read it before choosing a path.

What makes a custom ERPNext image take more effort to build?

Effort rises with four factors: the number of apps baked into the image, how much custom code those apps contain, how well their versions line up with your ERPNext major version, and how mature your CI process is. Each extra app adds a dependency to resolve and a test surface to cover. Custom code adds review, upgrade work and its own test data. Version mismatches between ERPNext 15 and 16 branches are a common source of failed builds. A team without automated builds will spend most of its time on manual rebuilds and rollbacks, so ask your partner or your own team to estimate effort per factor rather than in one total.

What does a fixed-scope engagement for a custom image include?

A fixed-scope engagement covers a written list of deliverables agreed before work starts. For this topic, that typically means an agreed app inventory, an apps.json definition, a build pipeline, a tagging and pinning convention, a test run against a staging site, and a short operations handover. Scope should also name what is excluded, such as custom application code, data migration or user training. Most engagements are fixed-scope, so the written scope is the basis for the quote, and DeployOpen does not publish a price list. Ask for the deliverables, the acceptance criteria and the assumptions the quote depends on.

What must our team provide before a custom image build starts?

The team has to provide access and decisions that only it can make. Typical inputs are:

  • Source repositories for every custom app, with branch rules and owners
  • Staging and test environments in your own cloud account, data center or private network
  • Anonymized or synthetic test data that resembles your accounting, stock and production records
  • Named approvers for app versions, release timing and production rollouts
  • Registry credentials and a secrets store for build secrets, which should never be written into the image

Missing approvers and unclear branch ownership cause more delays than technical issues, so settle them in the scope document first.

How do ERPNext upgrades change a custom Docker image?

An upgrade changes the image in two ways: the base ERPNext and Frappe versions move, and each custom app must support the new version. Frappe's supported versions page lists version 14 as end of life from January 31, 2026, with version 15 supported until the end of 2027 and version 16 until the end of 2029, so plan your target line early. Because both supported majors receive frequent minor releases, such as v16.51.1 and v15.123.0 in the ERPNext releases page, your image should be rebuilt on a regular schedule, not only when a new major appears. Pin tags and keep a rollback image for each release. Ask how your partner tests each new minor against your custom apps before promotion.

Does ERPNext's free license cover the cost of a custom image?

The license covers the software, not the work around it. ERPNext is GPL-3.0 licensed and the Frappe Framework is MIT licensed, and the ERPNext pricing page states that self-hosting ERPNext is free. The costs you still carry are staff or partner time to build and test the image, the infrastructure that runs builds and sites, registry storage, and the recurring work of rebuilding and upgrading. Hosting costs are listed on the pricing page for Frappe Cloud, where shared sites start at 5 US dollars per month, but a self-hosted image runs on your own infrastructure, so price it from your environment and the written scope. Implementation is available through certified partners, and the quote should separate one-time build costs from recurring maintenance.

How should build secrets and CI be handled for ERPNext images?

Build secrets, such as private repository tokens and registry credentials, should be passed to the build at runtime through your CI secret store and never copied into a layer. Layered images help here because the base ERPNext layer can be built once, while custom app layers are rebuilt when their code changes. Pin each image to an immutable tag, record the commit of every app in the image metadata, and scan images before they reach production. Confirm with your security team which scanner and retention policy apply, and document who can approve a promotion from staging to production.

How DeployOpen scopes and delivers a custom ERPNext image

A custom ERPNext image is only as dependable as the app list, version pins and build pipeline behind it. The cards below follow the work from agreeing the app set, through building and testing inside your infrastructure, to documenting the pipeline and supporting it afterwards.

Agreeing the app list and versions

DeployOpen starts by listing every app that goes into the image, including ERPNext, Frappe HR, India Compliance and any custom apps, and matching each one to a single Frappe major version. Apps built for different major versions cannot share a bench, so this check comes first. The written scope records each app's source repository, branch or tag, and the owner who approves changes to it.

Pinning tags and reproducible builds

Every app is pinned to a tag or commit rather than a moving branch, so the same inputs produce the same image on any rebuild. DeployOpen records these pins in the deliverables and checks them against the release notes of the ERPNext version you run. Ask how pins are updated, who approves a bump and how a rollback to the previous image is handled.

Build secrets and private repositories

Private custom apps and repositories that need tokens must not leak credentials into image layers or build logs. DeployOpen configures build-time secret injection, restricts which pipeline jobs can read them and confirms that no token remains in the final image history. Confirm with your security team where tokens are stored and how they are rotated.

Testing the image in your infrastructure

The built image is deployed to a staging environment in your cloud account or data center, where DeployOpen runs site migration, background workers, scheduler jobs and smoke tests against the apps you chose. Tests cover login, the apps' own DocTypes and any integration endpoints. Results are delivered as a test record, so you can see what passed before anything reaches production.

Pipeline documentation and handover

DeployOpen documents the pipeline as it is built: the build file, CI job definitions, required variables, the registry location and the steps to add or remove an app. Your engineers receive a runbook that explains how to rebuild, roll back and read a failed build. This documentation is part of the fixed-scope deliverables, not a separate follow-on item.

Optional retainer for ongoing support

Frappe publishes support windows for each major version, so the image needs regular rebuilds as ERPNext and app releases ship. A retainer can cover scheduled rebuilds, security patch review, test runs against new minor releases and help with upgrades between major versions. Ask what the retainer includes, how requests are prioritized and what is excluded before you sign.

Sources and further reading

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.