Mix Space + Shiro Full Containerized Deployment Guide
Mix Space is a modern personal space backend built on a front-end/back-end separation architecture; Shiro is its official companion high-aesthetic front-end theme, built on Next.js, renowned for its minimalist design and smooth animations. When used together, they provide you with a personal blog system that integrates article writing and daily updates, while delivering exceptional performance and visual appeal. — Gemini
I previously used Hexo, but struggled with difficult updates and limited functionality (because I prefer self-hosting and don't really like PaaS). Even updating a single character required rebuilding and re-uploading to the server, making fragmented writing very difficult.
Mix Space and Shiro complement each other, serving as the backend (Core) and frontend (Theme) respectively. There are also other frontend themes available (such as Kami and Yun). This article only covers Mix Space (backend) and Shiro (frontend).
For backend deployment, you can directly use the official pre-built Docker image. For the Shiro frontend, you can use either a pre-built Docker image or a self-built Docker image. Self-building allows deep modifications to the frontend pages (if you need to display two or more ICP filing numbers, self-building is mandatory).
Enough talk, let's get started!
Mix Space Deployment
Pull the Image
Backend deployment can be done simply by pulling the image with Docker Compose
cd && mkdir -p mx-space/core && cd $_
nano docker-compose.yml
Then write the following in docker-compose.yml:
services:
app:
container_name: mx-server
image: innei/mx-server:latest
environment:
- TZ=Asia/Shanghai
- NODE_ENV=production
- DB_HOST=mongo
- REDIS_HOST=redis
- ALLOWED_ORIGINS=localhost
- JWT_SECRET=YOUR_SUPER_SECURED_JWT_SECRET_STRING
volumes:
- ./data/mx-space:/root/.mx-space
ports:
- '2333:2333'
depends_on:
- mongo
- redis
networks:
- mx-space
restart: unless-stopped
healthcheck:
test: [CMD, curl, -f, 'http://127.0.0.1:2333/api/v2/ping']
interval: 1m30s
timeout: 30s
retries: 5
start_period: 30s
mongo:
container_name: mongo
image: mongo:7
volumes:
- ./data/db:/data/db
networks:
- mx-space
restart: unless-stopped
redis:
image: redis:alpine
container_name: redis
volumes:
- ./data/redis:/data
healthcheck:
test: [CMD-SHELL, 'redis-cli ping | grep PONG']
start_period: 20s
interval: 30s
retries: 5
timeout: 3s
networks:
- mx-space
restart: unless-stopped
networks:
mx-space:
driver: bridge
JWT_SECRETmust be a string with a length of no less than 16 characters and no more than 32 charactersALLOWED_ORIGINSshould contain the allowed domains, typically the frontend domain. If multiple domains are allowed, separate them with commas.
You can modify other settings as needed, for example changing '2333:2333' to '127.0.0.1:2333:2333'
Then you can start Core
docker compose up -d
Reverse Proxy
It's recommended to use modern panels like 1Panel or BT Panel for this, as it's very straightforward
Create a new website, configure the reverse proxy, set the URL to http://127.0.0.1:2333 and you're done
If you want to manually configure Nginx, refer to the official docs
Backend Configuration
Go to the website settings and set the Frontend URL, Admin Dashboard URL, API URL, and Gateway URL as follows:
- Frontend URL:
{Shiro URL} - Admin Dashboard URL:
{Backend URL}/proxy/qaqdmin - API URL:
{Backend URL}/api/v2 - Gateway URL:
{Backend URL}
For everything else, just configure as you see fit
Shiro Deployment
Go to the Mix Space admin panel, navigate to the "Configurations & Cloud Functions" page, click the add button in the top right corner, and fill in the following on the edit page:
- Name:
shiro - Reference:
theme - Data Type:
JSON - Data:
Modify the following content according to your needs
{
"footer": {
"otherInfo": {
"date": "202x-{{now}}",
"icp": {
"text": "萌 ICP 备 xxxxxxxx 号",
"link": "https://icp.gov.moe/?keyword=xxxxxxxx"
}
},
"linkSections": [
{
"name": "关于",
"links": [
{
"name": "关于本站",
"href": "/about-site"
},
{
"name": "关于我",
"href": "/about"
},
{
"name": "关于此项目",
"href": "https://github.com/innei/Shiro",
"external": true
}
]
},
{
"name": "更多",
"links": [
{
"name": "时间线",
"href": "/timeline"
},
{
"name": "友链",
"href": "/friends"
},
{
"name": "监控",
"href": "https://status.xxxxx.xxx",
"external": true
}
]
},
{
"name": "联系",
"links": [
{
"name": "写留言",
"href": "/message"
},
{
"name": "发邮件",
"href": "mailto:xxx@xxx.xxx",
"external": true
},
{
"name": "GitHub",
"href": "https://github.com/xxx",
"external": true
}
]
}
]
},
"config": {
"color": {
"light": [
"#33A6B8",
"#FF6666",
"#26A69A",
"#fb7287",
"#69a6cc",
"#F11A7B",
"#78C1F3",
"#FF6666",
"#7ACDF6"
],
"dark": [
"#F596AA",
"#A0A7D4",
"#ff7b7b",
"#99D8CF",
"#838BC6",
"#FFE5AD",
"#9BE8D8",
"#A1CCD1",
"#EAAEBA"
]
},
"bg": [
"/static/images/xxxxx.jpeg",
"/static/images/xxxxx.jpg"
],
"custom": {
"css": [],
"styles": [],
"js": [],
"scripts": []
},
"site": {
"favicon": "/xxxxx.svg",
"faviconDark": "/xxxxx-dark.svg"
},
"hero": {
"title": {
"template": [
{
"type": "h1",
"text": "xxxxxxx",
"class": "font-light text-4xl"
},
{
"type": "h1",
"text": "xxxxxxx",
"class": "font-medium mx-2 text-4xl"
},
{
"type": "h1",
"text": "👋。",
"class": "font-light text-4xl"
},
{
"type": "br"
},
{
"type": "h1",
"text": "xxxxxxx",
"class": "font-light text-4xl"
},
{
"type": "code",
"text": "<Developer />",
"class": "font-medium mx-2 text-3xl rounded p-1 bg-gray-200 dark:bg-gray-800/0 hover:dark:bg-gray-800/100 bg-opacity-0 hover:bg-opacity-100 transition-background duration-200"
},
{
"type": "span",
"class": "inline-block w-[1px] h-8 -bottom-2 relative bg-gray-800/80 dark:bg-gray-200/80 opacity-0 group-hover:opacity-100 transition-opacity duration-200 group-hover:animation-blink"
}
]
},
"description": "xxxxxxxxxxxxxxxxxxxx"
},
"module": {
"activity": {
"enable": true,
"endpoint": "/fn/ps/update"
},
"donate": {
"enable": true,
"link": "https://afdian.net/xxxxx",
"qrcode": [
"/static/images/xxxxxx.png",
"/static/images/xxxxxx.png"
]
},
"bilibili": {
"liveId": 1
}
}
}
}
Shiro frontend deployment can be divided into the official pre-built version and the self-built version. Choose according to your needs.
Pre-built is suitable for quickly setting up a site, but doesn't allow deep modifications. As mentioned: choose according to your needs.
One more thing — if you've sponsored the closed-source version Shiroi, you can only choose self-building, as the official team doesn't provide a pre-built image for it.
Pre-built
Pre-built is very quick — just pull the official image, configure the environment variables, and you're ready to go live
mkdir shiro && cd shiro
nano docker-compose.yml
services:
shiro:
container_name: shiro
image: innei/shiro:latest
volumes:
- ./.env:/app/.env
- ./public:/app/public
restart: always
environment:
- NEXT_SHARP_PATH=/usr/local/lib/node_modules/sharp
ports:
- 2323:2323
Configure environment variables
nano .env
Then fill in
NEXT_PUBLIC_API_URL=https://{backend-url}/api/v2
NEXT_PUBLIC_GATEWAY_URL=https://{backend-url}
Once filled in, you can run docker compose up -d
Self-built
Self-building is a bit more involved, but if you want deep customization, this is your only option
Here we use GitHub Actions to build the Docker image — convenient and fast
Go to your GitHub and create a repository. A private repository is recommended, as we'll need it shortly
Make sure you have Git installed on your computer
Open a terminal in a directory and run the following commands. Note: do not delete the .git folder. We use a "rename remote" approach so that you can sync and merge when the original author pushes updates later
# Clone the original author's code
git clone https://github.com/Innei/Shiro.git
cd Shiro
# Rename the original author's remote to "upstream"
git remote rename origin upstream
# Replace the URL below with your own GitHub repository URL
git remote add origin https://github.com/xxxxxx/xxxxx.git
Here's the key part! Navigate to and clear the .github / workflows folder, then create a new file docker-main.yml
And fill in the following:
name: Docker (Main Branch)
on:
workflow_dispatch:
push:
branches:
- main
repository_dispatch:
types: [trigger-workflow]
permissions: write-all
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PNPM_VERSION: 9.x.x
HASH_FILE: path/to/hash/file.txt
TZ: Asia/Shanghai
jobs:
prepare:
name: Prepare
runs-on: ubuntu-latest
if: ${{ github.event.head_commit.message != 'Update hash file' }}
outputs:
hash_content: ${{ steps.read_hash.outputs.hash_content }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Read HASH_FILE content
id: read_hash
run: |
content=$(cat ${{ env.HASH_FILE }}) || true
echo "hash_content=$content" >> "$GITHUB_OUTPUT"
check:
name: Check Should Rebuild
runs-on: ubuntu-latest
needs: prepare
outputs:
canceled: ${{ steps.use_content.outputs.canceled }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- name: Use content from prev job and compare
id: use_content
env:
FILE_HASH: ${{ needs.prepare.outputs.hash_content }}
run: |
file_hash=$FILE_HASH
current_hash=$(git rev-parse --short HEAD)
echo "File Hash: $file_hash"
echo "Current Git Hash: $current_hash"
if [ "$file_hash" == "$current_hash" ]; then
echo "Hashes match. Stopping workflow."
echo "canceled=true" >> $GITHUB_OUTPUT
else
echo "Hashes do not match. Continuing workflow."
fi
build:
name: Build artifact
runs-on: ubuntu-latest
needs: check
if: ${{ needs.check.outputs.canceled != 'true' }}
outputs:
sha_short: ${{ steps.store.outputs.sha_short }}
branch: ${{ steps.store.outputs.branch }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- name: Set up Image Name
run: |
IMAGE_ID=ghcr.io/${{ github.repository }}
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
echo "IMAGE_ID=$IMAGE_ID" >> $GITHUB_ENV
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Store artifact commit version
shell: bash
id: store
run: |
sha_short=$(git rev-parse --short HEAD)
branch_name=$(git rev-parse --abbrev-ref HEAD)
echo "sha_short=$sha_short" >> "$GITHUB_OUTPUT"
echo "branch=$branch_name" >> "$GITHUB_OUTPUT"
- name: Build Docker Image
run: |
docker build --build-arg TZ=Asia/Shanghai -t ${{ env.IMAGE_ID }}:${{ steps.store.outputs.sha_short }} .
- name: Push Docker Image to Github
run: |
docker push ${{ env.IMAGE_ID }}:${{ steps.store.outputs.sha_short }}
- name: Tag Docker Image as latest
run: |
docker tag ${{ env.IMAGE_ID }}:${{ steps.store.outputs.sha_short }} ${{ env.IMAGE_ID }}:latest
- name: Push Docker Image as latest
run: |
docker push ${{ env.IMAGE_ID }}:latest
Save and exit
Then return to the CMD window
# Make sure the branch name is main
git branch -M main
# Commit the first change (you can customize the message)
git add .
git commit -m "Hello World"
# Push to your repository
git push -u origin main
After pushing, go back to your GitHub repository to check

You can click the small yellow dot next to it to view the progress



After the build is complete, you still need to configure a GitHub Token on your server to pull the image
- Go to GitHub → Click your avatar → Settings → Developer Settings → Personal access tokens (classic) and create a Token
- The expiration can be set to never expire, and for permissions just set
read:packages - Go to your server and run
echo "{your-token}" | docker login ghcr.io -u {your-GitHub-ID} --password-stdin
Then follow the process as usual
mkdir shiro && cd shiro
nano docker-compose.yml
services:
shiro:
container_name: shiro
image: ghcr.io/{your-GitHub-ID}/{your-repo-name}:latest
volumes:
- ./.env:/app/.env
- ./public:/app/public
restart: always
environment:
- NEXT_SHARP_PATH=/usr/local/lib/node_modules/sharp
ports:
- 2323:2323
Configure environment variables
nano .env
Then fill in
NEXT_PUBLIC_API_URL=https://{backend-url}/api/v2
NEXT_PUBLIC_GATEWAY_URL=https://{backend-url}
Once filled in, you can run docker compose up -d
Alright, the entire process is now complete. How you want to tinker with it from here is up to you.
Updating Code
As the original author continues to update, you may want to sync the latest features. Thanks to the fact that we preserved the Git history and configured upstream earlier, updating becomes very simple.
If there are no conflicts, you only need three commands to complete the update
# Fetch the latest code from the original author
git fetch upstream
# Merge into your local branch
git merge upstream/main
# Push to your own repository
git push origin main
If there are conflicts, just head to VS Code to "mediate" them.
Closing Notes
If your server is located in mainland China, pulling images from ghcr.io may be very slow or even fail. To solve this problem, you can sync and push the image to Alibaba Cloud ACR after the build is complete.
Add the following to the env section in docker-main.yml
# Your Alibaba Cloud container registry address
ALIYUN_REGISTRY: crpi-xxxxxxxxx.cn-shanghai.personal.cr.aliyuncs.com
# Your namespace
ALIYUN_NAMESPACE: xxxxx
# Your repository name
ALIYUN_REPO: xxxxx
Then add the following at the end of the file
- name: Login to Aliyun ACR
uses: docker/login-action@v3
with:
registry: ${{ env.ALIYUN_REGISTRY }}
username: ${{ secrets.ALIYUN_USERNAME }}
password: ${{ secrets.ALIYUN_PASSWORD }}
- name: Tag & Push Image to Aliyun (Hash)
run: |
ALIYUN_IMAGE="${{ env.ALIYUN_REGISTRY }}/${{ env.ALIYUN_NAMESPACE }}/${{ env.ALIYUN_REPO }}:${{ steps.store.outputs.sha_short }}"
docker tag ${{ env.IMAGE_ID }}:${{ steps.store.outputs.sha_short }} $ALIYUN_IMAGE
docker push $ALIYUN_IMAGE
- name: Tag & Push Image to Aliyun (Latest)
run: |
ALIYUN_IMAGE_LATEST="${{ env.ALIYUN_REGISTRY }}/${{ env.ALIYUN_NAMESPACE }}/${{ env.ALIYUN_REPO }}:latest"
docker tag ${{ env.IMAGE_ID }}:latest $ALIYUN_IMAGE_LATEST
docker push $ALIYUN_IMAGE_LATEST
Next, configure your Alibaba Cloud account full name and the ACR instance's standalone login password as the ALIYUN_USERNAME and ALIYUN_PASSWORD variables in the repository's Actions Secrets respectively.
Once configured, the next time you push code, GitHub Actions will automatically sync the image to Alibaba Cloud. This way, servers in China will never have to spend half an hour pulling images again.