iTranslated by AI
Setting Up a Zenn Writing Environment with Dev Containers and Publishing via GitHub
Introduction
I usually write articles on Qiita, but I've decided to try posting on Zenn as well.
Zenn offers two ways to write: using the web editor or connecting with a GitHub repository. I chose the GitHub integration for this.
Managing content via GitHub is appealing, and I simply wanted to try it out.
However, I was a bit hesitant about installing Node.js and other tools locally.
I wanted to avoid cluttering my environment and ensure that I could easily reproduce the same state on another machine.
So, I decided to use Dev Container to containerize the entire writing environment.
By simply running VS Code's Reopen in Container, Zenn CLI and the preview server launch immediately. This allows me to write articles under the same conditions anywhere.
In this article, I will summarize the process from setting up the environment to actually publishing an article on Zenn.
Prerequisites
To follow the steps in this article, you will need the following:
- Docker
- VS Code
- GitHub account
- Zenn account
1. Prepare a GitHub Repository and Connect to Zenn
Zenn's official documentation provides a procedure for connecting a GitHub repository.
Basically, I will follow this flow.
Create a Repository on GitHub
Create an empty repository on GitHub to manage your articles. I named it zenn-content.
Connect via Zenn Deployment Settings
Open "GitHub integration" from the sidebar of your Zenn dashboard. By default, nothing is connected. Click the "Connect a repository" button to proceed to the GitHub authorization flow.

Upon proceeding, you will see a screen to choose which repository the Zenn Connect GitHub App is allowed to access. You don't need to grant access to everything, so I selected only the zenn-content repository I created earlier under "Only select repositories".

Confirm the Connection
Once the connection is complete, you will return to the Zenn screen, and the connected repository will be displayed in the "Repository settings" tab.

Once you reach this point, all you need to do is push your articles, and they will be reflected on Zenn.
2. Build the Dev Container
Now, let's assemble the Dev Container. As preparation on the VS Code side, first install the Dev Containers extension. With this installed, you can launch the container environment later by running Reopen in Container.
Directory Structure
Create a .devcontainer/ directory directly under the repository and place two files in it.
.devcontainer/
├── Dockerfile
└── devcontainer.json
Their roles are as follows:
-
Dockerfile— Defines the container image contents (base OS, Node.js, additional tools to install). -
devcontainer.json— Configures the Dev Container behavior (mounts, environment variables, VS Code extensions, etc.).
Dockerfile
The contents of .devcontainer/Dockerfile are as follows:
FROM node:24-trixie-slim
# Suppress interactive prompts during build & suppress npm update notifications
ENV DEBIAN_FRONTEND=noninteractive \
npm_config_update_notifier=false
WORKDIR /workspace
# For downloading Claude Code installation script & GitHub CLI apt repository
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# GitHub CLI
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
| tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh \
&& rm -rf /var/lib/apt/lists/*
USER node
# Claude Code
RUN curl -fsSL https://claude.ai/install.sh | bash
# For Zenn CLI preview server
EXPOSE 8000
CMD ["bash"]
I chose node:24-trixie-slim as the base. Since Node.js is required to run the Zenn CLI, I am using the official Node.js image. By choosing slim, the image size is also kept down.
In the first apt-get install, I am installing curl and ca-certificates. This is for subsequent steps (registering the GitHub CLI apt repository and installing Claude Code) where HTTPS-based downloads are needed.
The # GitHub CLI block is the setup to enable the use of the gh command from within the container. I included it because I want to complete repository operations and PR creation within the Dev Container. I am obtaining and registering the official gh apt repository signature key, then installing the gh package.
Since I want to utilize Claude Code for writing, I install Claude Code with RUN curl -fsSL https://claude.ai/install.sh | bash.
The final EXPOSE 8000 is for the Zenn CLI preview server. Combined with forwardPorts in the next devcontainer.json, I can view it from the host's browser.
devcontainer.json
The contents of .devcontainer/devcontainer.json are as follows:
{
"name": "Zenn Writing",
"build": {
"dockerfile": "Dockerfile"
},
"features": {
"ghcr.io/atsushi11o7/devcontainer-features/base-utils:2": {
"locale": "C.UTF-8",
"timezone": "Asia/Tokyo"
}
},
"workspaceFolder": "/workspace",
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
"runArgs": ["--env-file", "${localWorkspaceFolder}/.env"],
"mounts": [
"source=zenn-content-claude-config,target=/home/node/.claude,type=volume"
],
"containerEnv": {
"CLAUDE_CONFIG_DIR": "/home/node/.claude",
"LANG": "C.UTF-8",
"LC_ALL": "C.UTF-8"
},
"postCreateCommand": "test -f package.json && npm install || true",
"customizations": {
"vscode": {
"extensions": [
"anthropic.claude-code",
"yzhang.markdown-all-in-one",
"DavidAnson.markdownlint"
]
}
},
"remoteUser": "node",
"updateRemoteUserUID": true,
"forwardPorts": [8000]
}
I specified the Dockerfile from earlier in build.dockerfile.
I am including a custom Dev Container Feature in features. ghcr.io/atsushi11o7/devcontainer-features/base-utils:2 is a feature that sets up basic utilities like git, tzdata, and locales all at once. You don't need to use the exact same configuration, so please add any necessary tools yourself. git is used in this article's procedure, so if you don't use the feature, please add apt-get install -y git to the Dockerfile side.
With workspaceFolder and workspaceMount, I am bind-mounting the host repository to /workspace. The content edited inside the container is reflected directly on the host side.
The --env-file in runArgs is the setting to read the host's .env as container environment variables. This is the entry point for passing the token used for gh authentication into the container, and how to prepare .env will be covered in the next section.
mounts is mapping a Docker named volume (zenn-content-claude-config) to /home/node/.claude. This is a setting to persist Claude Code authentication information and configurations across container rebuilds, and containerEnv.CLAUDE_CONFIG_DIR is used to tell Claude Code to use this location.
The postCreateCommand test -f package.json && npm install || true is structured to run npm install only when package.json exists. This prevents errors during the initial build when package.json does not yet exist in the repository.
In customizations.vscode.extensions, I have specified the VS Code extensions that are automatically installed in the container.
-
anthropic.claude-code— Claude Code (writing assistance) -
yzhang.markdown-all-in-one— Markdown shortcuts and TOC generation -
DavidAnson.markdownlint— Warns about Markdown formatting inconsistencies or missing blank lines
By combining remoteUser: "node" and updateRemoteUserUID: true, I have enabled the behavior that matches the container-side user UID with the host side. This is to avoid common troubles where the owner of bind-mounted files differs between host and container, leading to writing permission issues.
Prepare .env and Authenticate gh
I will prepare the .env here, which is read by the runArgs: ["--env-file", "${localWorkspaceFolder}/.env"] setting in the previous section's devcontainer.json.
While the procedure in this article itself concludes with git push, being able to perform PR creation and release operations from within the Dev Container using gh makes subsequent operations easier. Therefore, I will pass the necessary GitHub Personal Access Token to the container via .env.
Since I assume everyone will create their own .env and write their own token, I will place a template .env.example in the repository.
GH_TOKEN=your-token
Since .env itself contains sensitive information, I will add it to .gitignore to exclude it from commits.
.env
If you issue a GitHub Personal Access Token (classic) with the repo scope and replace the your-token part of .env with your actual token, gh will be automatically authenticated upon container startup. After launching the container in the next section, you can run gh auth status to verify the login information.
Launch the Container
Once you have everything ready, open the repository in VS Code. If you have the Dev Containers extension installed, a "Reopen in Container" dialog will appear in the bottom right of the screen; just select it to start the container build and launch. If it does not appear, you can execute "Dev Containers: Reopen in Container" from the command palette (F1 or Ctrl/Cmd + Shift + P).
When complete, VS Code will attach to the container, and the terminal will also switch to the container side. Subsequent work can all be done inside the container.
3. Set up Zenn CLI
I will install the Zenn CLI from the terminal inside the container. First, create package.json and add zenn-cli as a dependency.
npm init -y
npm install zenn-cli
Next, run the Zenn CLI initialization command.
npx zenn init
This will create skeleton articles/ and books/ directories. You will write articles in Markdown inside articles/.
4. Write and Preview Articles
Create a New Article
Use npx zenn new:article to create an article template. For this article, I executed the following:
npx zenn new:article \
--slug zenn-publish-via-github-with-devcontainer \
--title "Dev Container で Zenn の執筆環境を整えて、GitHub 連携で記事を投稿するまで" \
--type tech \
--emoji 🚀
The options are as follows:
-
--slug— Identifier used for the article URL. It also becomes the filenamearticles/<slug>.md. -
--title— Article title. -
--type—tech(technical article) oridea(idea article). -
--emoji— Emoji displayed on the article thumbnail.
Executing this creates articles/<slug>.md, which contains the following frontmatter at the beginning:
---
title: "Dev Container で Zenn の執筆環境を整えて、GitHub 連携で記事を投稿するまで"
emoji: "🚀"
type: "tech"
topics: []
published: false
---
Set arbitrary tags (maximum of 5) in topics, and leave published as false until the time of publication. All that's left is to write the body in Markdown.
Launch the Preview Server
To check the appearance while writing, start the local server with npx zenn preview.
npx zenn preview
Since forwardPorts: [8000] in devcontainer.json is effective, you can open http://localhost:8000 in your browser to see the preview. VS Code also detects the port forwarding and sends a notification, so you can also open the browser directly from there.

5. Push and Reflect on Zenn
When you are finished writing, change published in the frontmatter to true. Also, include up to 5 tags in topics that match the content of the article.
---
title: "Dev Container で Zenn の執筆環境を整えて、GitHub 連携で記事を投稿するまで"
emoji: "🚀"
type: "tech"
topics: ["zenn", "devcontainer", "docker", "vscode", "github"]
published: true
---
All that remains is to push to GitHub.
git add articles/zenn-publish-via-github-with-devcontainer.md
git commit -m "Publish article"
git push
Triggered by the push, Zenn's deployment will run automatically, and after a short while, the article will be published on your Zenn dashboard and profile page. You can check the reflection status on the "Deployment History" tab of your Zenn dashboard.
Conclusion
Up to this point, I have summarized how to set up the Zenn writing environment using Dev Container and GitHub integration, and how to post your first article.
And this article is the first piece written in the environment created through these steps.
I am utilizing Claude Code for writing. Using Agents and Skill functions, I proceeded with creating drafts for each section, mechanical checks before publication, and reviews from an editor's perspective. I explain this writing workflow in more detail at Trying to Build a Zenn Writing Workflow with Claude Code's Skills and Agents.
Discussion