Halo's image hardcodes Asia/Shanghai and boots the app at build time
GitHub describes it as Halo 是一款强大易用的开源建站工具,从个人博客、知识库,到企业官网、在线商城,Halo 都能助您轻松实现,一站式满足您的多样化建站需求。. The repository metadata lists Java as its primary language. The metadata lists the GPL-3.0 license. This article stays within the project description and details documented in the GitHub repository README.
At a glance
- What is it?
- Halo is a GPLv3 Java site builder distributed as a Spring Boot container, and its Dockerfile is the most informative file in the repository. The image expects a jar somebody else compiled, pins the timezone to Asia/Shanghai, runs as root, and generates a class data sharing archive by starting the application during the build.
- Who is it for?
- Halo fits a team that wants a self-hosted Java CMS with a documented Docker path and a large theme and plugin catalogue, and it does not fit a deployment that has not thought about the timezone the image hardcodes or the root owned files it leaves in a home directory.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Java, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The Dockerfile expects a jar in build/libs and never invokes Gradle
The first thing the Dockerfile does is look for something that already exists. It declares ARG JAR_FILE=/application/build/libs/*.jar, copies whatever matches into the working directory as application.jar, and then runs the jar with the Spring Boot tools mode to extract it into layers. A stage called builder does not build anything, and there is no Gradle, no wrapper and no JDK in the image, only eclipse-temurin:21-jre in both stages.
So the image is a packaging step, and the compilation happens somewhere else. At the top level of the repository that somewhere else is visible: gradlew and gradlew.bat, build.gradle, settings.gradle, gradle.properties and a buildSrc/ directory, over a multi-module layout with api/, application/, platform/, ui/ and api-docs/. A clean clone plus this Dockerfile produces an image build error rather than an image.
The payoff for doing it this way is the layer split. The runtime stage copies four directories out of the extracted jar separately, dependencies, spring-boot-loader, snapshot-dependencies and application, which is the arrangement that lets a dependency layer be reused when only your own classes change. It is a real optimisation, and it is also why the build arg exists: the image cannot be rebuilt from source without the jar as an input.
TZ is Asia/Shanghai and the localtime link is made at build time
The runtime environment block is four variables in one declaration.
ENV JVM_OPTS="" \
HALO_WORK_DIR="/root/.halo2" \
SPRING_CONFIG_LOCATION="optional:classpath:/;optional:file:/root/.halo2/" \
TZ=Asia/ShanghaiThree of those are yours to change. The fourth is the interesting one, because a following RUN turns it into a real system link: it creates a symlink from /usr/share/zoneinfo/$TZ to /etc/localtime and writes $TZ into /etc/timezone.
Here is the consequence. That RUN executes while the image is being built, using the build-time value of TZ. Overriding TZ when you start the container changes what the JVM believes, because Java reads the environment variable, but it does not re-run the RUN, so /etc/localtime still points at Asia/Shanghai. Anything in the container that reads the system timezone rather than the Java property, which includes log timestamps produced by native code and any scheduled task that shells out, will be on Beijing time. Set the variable and check both.
There is no USER directive, so the container writes root owned files into your home directory
The quick start command in the README mounts a host directory straight into the container's data path.
docker run -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.26That works because the image never drops privileges. There is no USER line anywhere in the Dockerfile, the working directory is /application, and HALO_WORK_DIR points at /root/.halo2, so the process runs as root and everything it creates in the mounted volume belongs to root on the host. The port mapping is 8090 to 8090, which matches the EXPOSE in the image.
The other thing worth noticing is that the same path is both the work directory and the configuration location. SPRING_CONFIG_LOCATION is optional:classpath:/;optional:file:/root/.halo2/, so application configuration is read from the classpath first and then from the volume, which means a config file dropped into that directory on the host overrides what shipped in the jar. That is a useful arrangement and a sharp edge at the same time: deleting the volume deletes configuration, not just content.
A class data sharing archive is baked by starting the app during the build
One RUN in the Dockerfile launches the application, and the reason is visible in the flags. It runs java with -XX:ArchiveClassesAtExit=application.jsa and -Dspring.context.exit=onRefresh, points the work directory at a throwaway /tmp/halo2, and then deletes that directory in the same line.
This is a class data sharing archive. The JVM records which classes get loaded during a normal startup, and the next startup can map that recorded set into shared memory instead of loading it again. The runtime entrypoint consumes it:
ENTRYPOINT ["sh", "-c", "exec java ${JVM_OPTS} -XX:SharedArchiveFile=application.jsa -jar application.jar \"$@\"", "--"]The trade-offs are precise. The archive is generated from a run that was deliberately cut short at context refresh, so anything that loads later, including classes reached by a request rather than by startup, is not in it. And the archive is tied to the exact jar it was produced from, so swapping in a different application.jar without regenerating the .jsa file gives you a stale archive rather than a clear failure. Faster startup is the win; a build that has to be understood rather than trusted is the price.
The one-line Docker command is labelled trial use, and it pins 2.26
The README is explicit about the status of that run command. It says that if your machine has a Docker environment you can use it to start a Halo experience environment quickly, and then, in bold, that this method is only for trial use. The recommended route is an open source Linux server management panel called 1Panel, with documentation covering reverse proxy, SSL certificates, upgrade and backup tasks, and further deployment methods in the install guide.
That is a sensible warning, because the one-liner leaves out everything a long-lived site needs: a reverse proxy in front of it, certificates, a backup schedule and an upgrade plan. It also pins halohub/halo:2.26, while the recent releases are 2.26.0 from 2026-08-14, 2.26.1 from 2026-09-01 and a 2.27.0-beta.1 from 2026-09-24, so the documented command is one patch behind the current stable line.
The last push to main was on 2026-09-29, a few days after that beta. Anyone evaluating the platform should read the install documentation and the 1Panel guide rather than treating the trial command as the supported deployment.
The open source README is also the shop front for two paid editions
The README compares three editions, and only one of them is what the repository builds. The community edition is open source and free under GPLv3, aimed at individual developers, technical enthusiasts and open source projects, with zero cost blogs, portfolios and documentation sites, and the project claims more than 100 free themes and plugins.
The professional edition sits on top of it and adds what it calls 10+ high value features: a mobile app for managing content on the move, AI assisted site building, login by phone number, private deployment across the whole site, and a paid theme and plugin market with 10 paid plugins covering premium themes, SEO optimisation, paid reading and an AI assistant. The commerce edition adds an integrated online store with product management, order handling and payment integration, and it is explicitly tailored for Chinese merchants with WeChat Pay and Alipay.
The detail that catches the eye is where the comparison lives. The link is not on halo.run, it points at lxware.cn. The decision document for a GPLv3 project and its two commercial tiers sits on a separate domain from the project's own website, docs and community forum.
The licence badge points at master and the contributing link at main
The repository root carries a governance layer that is worth reading before contributing: OWNERS, CODE_OF_CONDUCT.md, SECURITY.md, CONTRIBUTING.md, AGENTS.md and CLAUDE.md, plus .claude/, .codex/ and .vscode/ directories, an openspec/ directory and a hack/ directory. That is a project organised around written specifications and machine instructions rather than only around source.
It is also distributed through more than one host. The README links a Gitee organisation, a GitCode mirror under the name feizhiyun, a Telegram channel, a community forum at bbs.halo.run, a documentation site at docs.halo.run and a codecov page. The app ecosystem has its own store, an onboarding page for developers publishing their own themes and plugins, and a curated list at halo-sigs/awesome-halo.
The small inconsistency worth flagging is a branch name. The licence badge links to a LICENSE file on master, while the contributing link points at CONTRIBUTING.md on main, and the default branch is main. On a repository where the two names coexist, one of those links is stale, and it is the licence link.
Editorial conclusion
Halo fits a team that wants a self-hosted Java CMS with a documented Docker path and a large theme and plugin catalogue, and it does not fit a deployment that has not thought about the timezone the image hardcodes or the root owned files it leaves in a home directory. Verify first that you can produce a jar, because the Dockerfile expects one in build/libs and never runs Gradle, then read the install documentation rather than the trial command, and note that the current stable release is 2.26.1 while that command pins 2.26.
Frequently asked questions
What is Halo, the open source site builder?
Halo is a GPLv3 open source website building tool written in Java, described as covering personal blogs, knowledge bases, enterprise sites and online stores. The site is halo.run, the documentation is docs.halo.run and the community forum is bbs.halo.run. The community edition is free and the README states there are more than 100 free themes and plugins.
How do I try Halo without installing it?
There is a public demo at demo.halocms.site with an admin console at the /console path, and the README publishes a fixed username and password for it. Because those credentials are published in the repository, change them before you point anything real at an instance you run yourself.
How do I run Halo with Docker?
The quick start command is docker run -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.26, which publishes port 8090 and mounts the data and configuration directory. The README says that route is for trial use only and points to the 1Panel panel and the install documentation for reverse proxy, certificates, upgrades and backups.
What are the Halo editions?
The community edition is open source and free under GPLv3. The professional edition adds 10 or more features including a mobile app, AI site building, login by phone number, private deployment and a paid theme and plugin market with 10 paid plugins. The commerce edition adds an integrated online store with product, order and payment handling, including WeChat Pay and Alipay.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/halo-dev-halo)